From ea471f5601e91ccd60dab1200a163d6305e12bab Mon Sep 17 00:00:00 2001 From: mehmet turac Date: Fri, 19 Jun 2026 12:27:23 +0300 Subject: [PATCH] fix(firefox): disable app updates with policies --- .../src/server/firefox/firefox.ts | 8 +- .../src/server/firefox/firefoxPolicies.ts | 66 +++++++++++++++ tests/library/firefox/launcher.spec.ts | 84 +++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 packages/playwright-core/src/server/firefox/firefoxPolicies.ts diff --git a/packages/playwright-core/src/server/firefox/firefox.ts b/packages/playwright-core/src/server/firefox/firefox.ts index 3e0b505951500..2c31f96fe3f0f 100644 --- a/packages/playwright-core/src/server/firefox/firefox.ts +++ b/packages/playwright-core/src/server/firefox/firefox.ts @@ -22,6 +22,7 @@ import { ManualPromise } from '@isomorphic/manualPromise'; import { wrapInASCIIBox } from '@utils/ascii'; import { FFBrowser } from './ffBrowser'; import { kBrowserCloseMessageId } from './ffConnection'; +import { amendFirefoxPoliciesEnv, prepareFirefoxPolicies } from './firefoxPolicies'; import { BrowserType, kNoXServerRunningError } from '../browserType'; import type { Browser, BrowserOptions } from '../browser'; @@ -57,6 +58,10 @@ export class Firefox extends BrowserType { return FFBrowser.connect(this.attribution.playwright, transport, options); } + override async prepareUserDataDir(options: types.LaunchOptions, userDataDir: string): Promise { + await prepareFirefoxPolicies(options, userDataDir); + } + override doRewriteStartupLog(logs: string): string { // https://github.com/microsoft/playwright/issues/6500 if (logs.includes(`as root in a regular user's session is not supported.`)) @@ -66,7 +71,8 @@ export class Firefox extends BrowserType { return logs; } - override amendEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + override amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string): NodeJS.ProcessEnv { + env = amendFirefoxPoliciesEnv(env, userDataDir); if (!path.isAbsolute(os.homedir())) throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`); if (os.platform() === 'linux') { diff --git a/packages/playwright-core/src/server/firefox/firefoxPolicies.ts b/packages/playwright-core/src/server/firefox/firefoxPolicies.ts new file mode 100644 index 0000000000000..c573e8854f4f9 --- /dev/null +++ b/packages/playwright-core/src/server/firefox/firefoxPolicies.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; + +import { envArrayToObject } from '@utils/processLauncher'; + +export const kFirefoxPoliciesEnvName = 'PLAYWRIGHT_FIREFOX_POLICIES_JSON'; + +type FirefoxPoliciesOptions = { + env?: { name: string, value: string }[]; +}; + +type FirefoxPolicies = { + policies?: Record; + [key: string]: unknown; +}; + +export function firefoxPoliciesPath(userDataDir: string): string { + return path.join(userDataDir, 'playwright-policies.json'); +} + +export async function prepareFirefoxPolicies(options: FirefoxPoliciesOptions, userDataDir: string): Promise { + const env = options.env ? envArrayToObject(options.env) : process.env; + const userPolicies = await readFirefoxPolicies(env[kFirefoxPoliciesEnvName]); + const policies: FirefoxPolicies = { + ...userPolicies, + policies: { + ...userPolicies.policies, + // Keep Playwright-managed Firefox builds from starting their own update service. + DisableAppUpdate: true, + }, + }; + await fs.promises.writeFile(firefoxPoliciesPath(userDataDir), JSON.stringify(policies)); +} + +export function amendFirefoxPoliciesEnv(env: NodeJS.ProcessEnv, userDataDir: string): NodeJS.ProcessEnv { + return { + ...env, + [kFirefoxPoliciesEnvName]: firefoxPoliciesPath(userDataDir), + }; +} + +async function readFirefoxPolicies(policiesPath: string | undefined): Promise { + if (!policiesPath) + return {}; + try { + return JSON.parse(await fs.promises.readFile(policiesPath, 'utf8')); + } catch (error) { + throw new Error(`Failed to read Firefox policies from ${policiesPath}: ${error}`); + } +} diff --git a/tests/library/firefox/launcher.spec.ts b/tests/library/firefox/launcher.spec.ts index 740c9ccd97316..9041f254db8f4 100644 --- a/tests/library/firefox/launcher.spec.ts +++ b/tests/library/firefox/launcher.spec.ts @@ -15,9 +15,11 @@ */ import fs from 'fs'; +import path from 'path'; import { playwrightTest as it, expect } from '../../config/browserTest'; import { TestServer } from '../../config/testserver'; import { inheritAndCleanEnv } from '../../config/utils'; +import { amendFirefoxPoliciesEnv, kFirefoxPoliciesEnvName, prepareFirefoxPolicies } from '../../../packages/playwright-core/src/server/firefox/firefoxPolicies'; it('should pass firefox user preferences', async ({ browserType, mode }) => { const browser = await browserType.launch({ @@ -73,3 +75,85 @@ it('should support custom firefox policies', async ({ browserType, mode, asset, await browser.close(); await server.stop(); }); + +it('should launch with generated firefox policies file', async ({ browserType, createUserDataDir }, testInfo) => { + const policiesPath = testInfo.outputPath('policies.json'); + await fs.promises.writeFile(policiesPath, JSON.stringify({ + policies: { + DisableTelemetry: true, + }, + })); + + const userDataDir = await createUserDataDir(); + const context = await browserType.launchPersistentContext(userDataDir, { + env: inheritAndCleanEnv({ [kFirefoxPoliciesEnvName]: policiesPath }), + }); + const generatedPolicies = JSON.parse(await fs.promises.readFile(path.join(userDataDir, 'playwright-policies.json'), 'utf8')); + expect(generatedPolicies).toEqual({ + policies: { + DisableTelemetry: true, + DisableAppUpdate: true, + }, + }); + await context.close(); +}); + +it('should merge custom firefox policies with disabled app updates', async ({}, testInfo) => { + const defaultUserDataDir = testInfo.outputPath('default-profile'); + await fs.promises.mkdir(defaultUserDataDir); + await prepareFirefoxPolicies({}, defaultUserDataDir); + expect(JSON.parse(await fs.promises.readFile(path.join(defaultUserDataDir, 'playwright-policies.json'), 'utf8'))).toEqual({ + policies: { + DisableAppUpdate: true, + }, + }); + + const policiesPath = testInfo.outputPath('policies.json'); + await fs.promises.writeFile(policiesPath, JSON.stringify({ + policies: { + Certificates: { + Install: ['cert.pem'], + }, + DisableAppUpdate: false, + }, + })); + + const userDataDir = testInfo.outputPath('profile'); + await fs.promises.mkdir(userDataDir); + await prepareFirefoxPolicies({ + env: [{ name: kFirefoxPoliciesEnvName, value: policiesPath }], + }, userDataDir); + + const generatedPolicies = JSON.parse(await fs.promises.readFile(path.join(userDataDir, 'playwright-policies.json'), 'utf8')); + expect(generatedPolicies).toEqual({ + policies: { + Certificates: { + Install: ['cert.pem'], + }, + DisableAppUpdate: true, + }, + }); +}); + +it('should point Firefox to generated policies file', async ({}, testInfo) => { + const userDataDir = testInfo.outputPath('profile'); + expect(amendFirefoxPoliciesEnv({}, userDataDir)).toEqual({ + [kFirefoxPoliciesEnvName]: path.join(userDataDir, 'playwright-policies.json'), + }); +}); + +it('should report invalid custom firefox policies', async ({}, testInfo) => { + const userDataDir = testInfo.outputPath('profile'); + await fs.promises.mkdir(userDataDir); + + const missingPoliciesPath = testInfo.outputPath('missing-policies.json'); + await expect(prepareFirefoxPolicies({ + env: [{ name: kFirefoxPoliciesEnvName, value: missingPoliciesPath }], + }, userDataDir)).rejects.toThrow(`Failed to read Firefox policies from ${missingPoliciesPath}`); + + const invalidPoliciesPath = testInfo.outputPath('invalid-policies.json'); + await fs.promises.writeFile(invalidPoliciesPath, '{'); + await expect(prepareFirefoxPolicies({ + env: [{ name: kFirefoxPoliciesEnvName, value: invalidPoliciesPath }], + }, userDataDir)).rejects.toThrow(`Failed to read Firefox policies from ${invalidPoliciesPath}`); +});