diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2f9e12ca0cda3..791a126efe7f2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -272,8 +272,12 @@ jobs: - name: Install Chrome Stable run: sudo apt install google-chrome-stable - run: npm ci + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - run: npm run build - run: node lib/cli/cli install-deps chromium + # This only created problems, should we move ffmpeg back into npm? + - run: node lib/cli/cli install ffmpeg - run: mkdir -p coredumps # Set core dump file name pattern - run: sudo bash -c 'echo "$(pwd -P)/coredumps/core-pid_%p.dump" > /proc/sys/kernel/core_pattern' diff --git a/docs/src/api/class-browsertype.md b/docs/src/api/class-browsertype.md index 34f0baea9d10d..499fdb5721bd8 100644 --- a/docs/src/api/class-browsertype.md +++ b/docs/src/api/class-browsertype.md @@ -299,6 +299,19 @@ Whether to run browser in headless mode. More details for [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the [`option: devtools`] option is `true`. +### option: BrowserType.launchPersistentContext.channel +- `channel` <[string]> + +Chromium distribution channel, one of +* chrome +* chrome-beta +* chrome-dev +* chrome-canary +* msedge +* msedge-beta +* msedge-dev +* msedge-canary + ### option: BrowserType.launchPersistentContext.executablePath - `executablePath` <[path]> diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 050763ce3ae28..2476e1f35c9af 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -85,7 +85,7 @@ program .description('ensure browsers necessary for this version of Playwright are installed') .action(async function(browserType) { try { - const allBrowsers = new Set(['chromium', 'firefox', 'webkit']); + const allBrowsers = new Set(['chromium', 'firefox', 'webkit', 'ffmpeg']); for (const type of browserType) { if (!allBrowsers.has(type)) { console.log(`Invalid browser name: '${type}'. Expecting 'chromium', 'firefox' or 'webkit'.`); @@ -186,6 +186,7 @@ else type Options = { browser: string; + channel?: string; colorScheme?: string; device?: string; geolocation?: string; @@ -209,6 +210,9 @@ async function launchContext(options: Options, headless: boolean): Promise<{ bro validateOptions(options); const browserType = lookupBrowserType(options); const launchOptions: LaunchOptions = { headless }; + if (options.channel) + launchOptions.channel = options.channel; + const contextOptions: BrowserContextOptions = // Copy the device descriptor since we have to compare and modify the options. options.device ? { ...playwright.devices[options.device] } : {}; @@ -452,6 +456,7 @@ function commandWithOpenOptions(command: string, description: string, options: a result = result.option(option[0], ...option.slice(1)); return result .option('-b, --browser ', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium') + .option('--channel ', 'Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc') .option('--color-scheme ', 'emulate preferred color scheme, "light" or "dark"') .option('--device ', 'emulate device, for example "iPhone 11"') .option('--geolocation ', 'specify geolocation coordinates, for example "37.819722,-122.478611"') diff --git a/src/client/browserType.ts b/src/client/browserType.ts index b5d5bbdaa1a37..b5d88b84cf34e 100644 --- a/src/client/browserType.ts +++ b/src/client/browserType.ts @@ -100,6 +100,7 @@ export class BrowserType extends ChannelOwner Validator): Scheme { slowMo: tOptional(tNumber), }); scheme.BrowserTypeLaunchPersistentContextParams = tObject({ + channel: tOptional(tString), userDataDir: tString, sdkLanguage: tString, executablePath: tOptional(tString), diff --git a/src/server/browser.ts b/src/server/browser.ts index ef14ed315541e..faebc1674c019 100644 --- a/src/server/browser.ts +++ b/src/server/browser.ts @@ -39,6 +39,7 @@ export type PlaywrightOptions = { export type BrowserOptions = PlaywrightOptions & { name: string, isChromium: boolean, + channel?: string, downloadsPath?: string, headful?: boolean, persistent?: types.BrowserContextOptions, // Undefined means no persistent context. diff --git a/src/server/browserType.ts b/src/server/browserType.ts index aaa555f31d32e..81c0e0a81bb42 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -104,6 +104,7 @@ export abstract class BrowserType extends SdkObject { ...this._playwrightOptions, name: this._name, isChromium: this._name === 'chromium', + channel: options.channel, slowMo: options.slowMo, persistent, headful: !options.headless, @@ -176,7 +177,7 @@ export abstract class BrowserType extends SdkObject { throw new Error(errorMessageLines.join('\n')); } - if (!executablePath) { + if (!executable) { // We can only validate dependencies for bundled browsers. await validateHostRequirements(this._registry, this._name); } diff --git a/src/server/chromium/findChromiumChannel.ts b/src/server/chromium/findChromiumChannel.ts index 260e1488787b9..82df79d23e938 100644 --- a/src/server/chromium/findChromiumChannel.ts +++ b/src/server/chromium/findChromiumChannel.ts @@ -20,6 +20,8 @@ import path from 'path'; function darwin(channel: string): string | undefined { switch (channel) { case 'chrome': return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + case 'chrome-beta': return '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta'; + case 'chrome-dev': return '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev'; case 'chrome-canary': return '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'; case 'msedge': return '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'; case 'msedge-beta': return '/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta'; @@ -41,6 +43,8 @@ function win32(channel: string): string | undefined { let suffix: string | undefined; switch (channel) { case 'chrome': suffix = `\\Google\\Chrome\\Application\\chrome.exe`; break; + case 'chrome-beta': suffix = `\\Google\\Chrome Beta\\Application\\chrome.exe`; break; + case 'chrome-dev': suffix = `\\Google\\Chrome Dev\\Application\\chrome.exe`; break; case 'chrome-canary': suffix = `\\Google\\Chrome SxS\\Application\\chrome.exe`; break; case 'msedge': suffix = `\\Microsoft\\Edge\\Application\\msedge.exe`; break; case 'msedge-beta': suffix = `\\Microsoft\\Edge Beta\\Application\\msedge.exe`; break; diff --git a/src/server/supplements/recorder/csharp.ts b/src/server/supplements/recorder/csharp.ts index bf2c6ed71ed10..f3108be6b4932 100644 --- a/src/server/supplements/recorder/csharp.ts +++ b/src/server/supplements/recorder/csharp.ts @@ -134,7 +134,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator { formatter.add(` await Playwright.InstallAsync(); using var playwright = await Playwright.CreateAsync(); - await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatArgs(options.launchOptions)}); + await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatArgs(options.launchOptions)} + ); var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`); return formatter.format(); } @@ -178,10 +179,7 @@ function formatArgs(value: any, indent = ' '): string { const tokens: string[] = []; for (const key of keys) tokens.push(`${keys.length !== 1 ? indent : ''}${key}: ${formatObject(value[key], indent, key)}`); - if (keys.length === 1) - return `${tokens.join(`,\n${indent}`)}`; - else - return `\n${indent}${tokens.join(`,\n${indent}`)}`; + return `\n${indent}${tokens.join(`,\n${indent}`)}`; } return String(value); } @@ -271,7 +269,7 @@ class CSharpFormatter { return this._lines.map((line: string) => { if (line === '') return line; - if (line.startsWith('}') || line.startsWith(']') || line.includes('});')) + if (line.startsWith('}') || line.startsWith(']') || line.includes('});') || line === ');') spaces = spaces.substring(this._baseIndent.length); const extraSpaces = /^(for|while|if).*\(.*\)$/.test(previousLine) ? this._baseIndent : ''; diff --git a/src/server/supplements/recorder/java.ts b/src/server/supplements/recorder/java.ts index 39689499e7dd3..b61633337e592 100644 --- a/src/server/supplements/recorder/java.ts +++ b/src/server/supplements/recorder/java.ts @@ -164,6 +164,8 @@ function formatLaunchOptions(options: any): string { lines.push('new BrowserType.LaunchOptions()'); if (typeof options.headless === 'boolean') lines.push(` .setHeadless(false)`); + if (options.channel) + lines.push(` .setChannel("${options.channel}")`); return lines.join('\n'); } diff --git a/src/server/supplements/recorder/recorderApp.ts b/src/server/supplements/recorder/recorderApp.ts index 6c5de1f98e21e..fd58fe831a34b 100644 --- a/src/server/supplements/recorder/recorderApp.ts +++ b/src/server/supplements/recorder/recorderApp.ts @@ -102,6 +102,7 @@ export class RecorderApp extends EventEmitter { if (isUnderTest()) args.push(`--remote-debugging-port=0`); const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), '', { + channel: inspectedContext._browser.options.channel, sdkLanguage: inspectedContext._options.sdkLanguage, args, noDefaultViewport: true, diff --git a/test/browsertype-basic.spec.ts b/test/browsertype-basic.spec.ts index 1d16181b8353f..09daa7e25b352 100644 --- a/test/browsertype-basic.spec.ts +++ b/test/browsertype-basic.spec.ts @@ -18,9 +18,11 @@ import fs from 'fs'; import { it, expect } from './fixtures'; -it('browserType.executablePath should work', test => { +it('browserType.executablePath should work', (test, { browserChannel }) => { + test.fixme(!!browserChannel, 'Uncomment on roll'); test.skip(Boolean(process.env.CRPATH || process.env.FFPATH || process.env.WKPATH)); -}, async ({browserType}) => { +}, async ({ browserType, browserChannel }) => { + // Interesting, unless I use browserChannel in test, filter above does not work! const executablePath = browserType.executablePath(); expect(fs.existsSync(executablePath)).toBe(true); }); diff --git a/test/browsertype-launch-server.spec.ts b/test/browsertype-launch-server.spec.ts index 7b22f7564b4fe..a441a6f505991 100644 --- a/test/browsertype-launch-server.spec.ts +++ b/test/browsertype-launch-server.spec.ts @@ -60,7 +60,9 @@ describe('launch server', (suite, { mode }) => { await browserServer.close(); }); - it('should fire close event', async ({browserType, browserOptions}) => { + it('should fire close event', (test, { browserChannel }) => { + test.fixme(!!browserChannel, 'Uncomment on roll'); + }, async ({browserType, browserOptions}) => { const browserServer = await browserType.launchServer(browserOptions); const [result] = await Promise.all([ // @ts-expect-error The signal parameter is not documented. diff --git a/test/cli/cli-codegen-csharp.spec.ts b/test/cli/cli-codegen-csharp.spec.ts index e96d5ac8232db..1f13afac2a9af 100644 --- a/test/cli/cli-codegen-csharp.spec.ts +++ b/test/cli/cli-codegen-csharp.spec.ts @@ -21,24 +21,28 @@ import { folio } from './cli.fixtures'; const { it, expect } = folio; const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString(); +const launchOptions = (channel: string) => { + return channel ? `headless: false,\n channel: "${channel}"` : 'headless: false'; +}; function capitalize(browserName: string): string { return browserName[0].toUpperCase() + browserName.slice(1); } -it('should print the correct imports and context options', async ({ browserName, runCLI }) => { - const cli = runCLI(['codegen', '--target=csharp', emptyHTML]); +it('should print the correct imports and context options', async ({ browserName, browserChannel, runCLI }) => { + const cli = runCLI(['--target=csharp', emptyHTML]); const expectedResult = `await Playwright.InstallAsync(); using var playwright = await Playwright.CreateAsync(); -await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(headless: false); +await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync( + ${launchOptions(browserChannel)} +); var context = await browser.NewContextAsync();`; await cli.waitFor(expectedResult).catch(e => e); expect(cli.text()).toContain(expectedResult); }); -it('should print the correct context options for custom settings', async ({ browserName, runCLI }) => { +it('should print the correct context options for custom settings', async ({ browserName, browserChannel, runCLI }) => { const cli = runCLI([ - 'codegen', '--color-scheme=dark', '--geolocation=37.819722,-122.478611', '--lang=es', @@ -51,11 +55,12 @@ it('should print the correct context options for custom settings', async ({ brow const expectedResult = `await Playwright.InstallAsync(); using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync( - headless: false, + ${launchOptions(browserChannel)}, proxy: new ProxySettings { Server = "http://myproxy:3128", - }); + } +); var context = await browser.NewContextAsync( viewport: new ViewportSize { @@ -78,21 +83,22 @@ var context = await browser.NewContextAsync( it('should print the correct context options when using a device', (test, { browserName }) => { test.skip(browserName !== 'chromium'); -}, async ({ runCLI }) => { - const cli = runCLI(['codegen', '--device=Pixel 2', '--target=csharp', emptyHTML]); +}, async ({ browserChannel, runCLI }) => { + const cli = runCLI(['--device=Pixel 2', '--target=csharp', emptyHTML]); const expectedResult = `await Playwright.InstallAsync(); using var playwright = await Playwright.CreateAsync(); -await using var browser = await playwright.Chromium.LaunchAsync(headless: false); +await using var browser = await playwright.Chromium.LaunchAsync( + ${launchOptions(browserChannel)} +); var context = await browser.NewContextAsync(playwright.Devices["Pixel 2"]);`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); }); -it('should print the correct context options when using a device and additional options', (test, {browserName}) => { +it('should print the correct context options when using a device and additional options', (test, { browserName }) => { test.skip(browserName !== 'webkit'); -}, async ({ runCLI }) => { +}, async ({ browserChannel, runCLI }) => { const cli = runCLI([ - 'codegen', '--device=iPhone 11', '--color-scheme=dark', '--geolocation=37.819722,-122.478611', @@ -106,11 +112,12 @@ it('should print the correct context options when using a device and additional const expectedResult = `await Playwright.InstallAsync(); using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.Webkit.LaunchAsync( - headless: false, + ${launchOptions(browserChannel)}, proxy: new ProxySettings { Server = "http://myproxy:3128", - }); + } +); var context = await browser.NewContextAsync(new BrowserContextOptions(playwright.Devices["iPhone 11"]) { UserAgent = "hardkodemium", @@ -134,15 +141,18 @@ var context = await browser.NewContextAsync(new BrowserContextOptions(playwright expect(cli.text()).toContain(expectedResult); }); -it('should print load/save storageState', async ({ browserName, runCLI, testInfo }) => { +it('should print load/save storageState', async ({ browserName, browserChannel, runCLI, testInfo }) => { const loadFileName = testInfo.outputPath('load.json'); const saveFileName = testInfo.outputPath('save.json'); await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8'); - const cli = runCLI(['codegen', `--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=csharp', emptyHTML]); + const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=csharp', emptyHTML]); const expectedResult1 = `await Playwright.InstallAsync(); using var playwright = await Playwright.CreateAsync(); -await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(headless: false); -var context = await browser.NewContextAsync(storageState: "${loadFileName}");`; +await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync( + ${launchOptions(browserChannel)} +); +var context = await browser.NewContextAsync( + storageState: "${loadFileName}");`; await cli.waitFor(expectedResult1); const expectedResult2 = ` diff --git a/test/cli/cli-codegen-java.spec.ts b/test/cli/cli-codegen-java.spec.ts index 03ccfd3e290f1..5062ead0a6bcd 100644 --- a/test/cli/cli-codegen-java.spec.ts +++ b/test/cli/cli-codegen-java.spec.ts @@ -21,9 +21,12 @@ import { folio } from './cli.fixtures'; const { it, expect } = folio; const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString(); +const launchOptions = (channel: string) => { + return channel ? `.setHeadless(false)\n .setChannel("${channel}")` : '.setHeadless(false)'; +}; -it('should print the correct imports and context options', async ({ runCLI, browserName }) => { - const cli = runCLI(['codegen', '--target=java', emptyHTML]); +it('should print the correct imports and context options', async ({ runCLI, browserChannel, browserName }) => { + const cli = runCLI(['--target=java', emptyHTML]); const expectedResult = `import com.microsoft.playwright.*; import com.microsoft.playwright.options.*; @@ -31,14 +34,14 @@ public class Example { public static void main(String[] args) { try (Playwright playwright = Playwright.create()) { Browser browser = playwright.${browserName}().launch(new BrowserType.LaunchOptions() - .setHeadless(false)); + ${launchOptions(browserChannel)}); BrowserContext context = browser.newContext();`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); }); it('should print the correct context options for custom settings', async ({ runCLI, browserName }) => { - const cli = runCLI(['codegen', '--color-scheme=light', '--target=java', emptyHTML]); + const cli = runCLI(['--color-scheme=light', '--target=java', emptyHTML]); const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() .setColorScheme(ColorScheme.LIGHT));`; await cli.waitFor(expectedResult); @@ -48,7 +51,7 @@ it('should print the correct context options for custom settings', async ({ runC it('should print the correct context options when using a device', (test, { browserName }) => { test.skip(browserName !== 'chromium'); }, async ({ runCLI }) => { - const cli = runCLI(['codegen', '--device=Pixel 2', '--target=java', emptyHTML]); + const cli = runCLI(['--device=Pixel 2', '--target=java', emptyHTML]); const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() .setUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36") .setViewportSize(411, 731) @@ -62,7 +65,7 @@ it('should print the correct context options when using a device', (test, { brow it('should print the correct context options when using a device and additional options', (test, { browserName }) => { test.skip(browserName !== 'webkit'); }, async ({ runCLI }) => { - const cli = runCLI(['codegen', '--color-scheme=light', '--device=iPhone 11', '--target=java', emptyHTML]); + const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', '--target=java', emptyHTML]); const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() .setColorScheme(ColorScheme.LIGHT) .setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1") @@ -78,7 +81,7 @@ it('should print load/save storage_state', async ({ runCLI, browserName, testInf const loadFileName = testInfo.outputPath('load.json'); const saveFileName = testInfo.outputPath('save.json'); await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8'); - const cli = runCLI(['codegen', `--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=java', emptyHTML]); + const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=java', emptyHTML]); const expectedResult1 = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() .setStorageStatePath(Paths.get("${loadFileName}")));`; await cli.waitFor(expectedResult1); diff --git a/test/cli/cli-codegen-javascript.spec.ts b/test/cli/cli-codegen-javascript.spec.ts index 2a212800d3ef3..ab6ff25915f74 100644 --- a/test/cli/cli-codegen-javascript.spec.ts +++ b/test/cli/cli-codegen-javascript.spec.ts @@ -22,26 +22,30 @@ const { it, expect } = folio; const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString(); -it('should print the correct imports and context options', async ({ browserName, runCLI }) => { - const cli = runCLI(['codegen', emptyHTML]); +const launchOptions = (channel: string) => { + return channel ? `headless: false,\n channel: '${channel}'` : 'headless: false'; +}; + +it('should print the correct imports and context options', async ({ browserName, browserChannel, runCLI }) => { + const cli = runCLI([emptyHTML]); const expectedResult = `const { ${browserName} } = require('playwright'); (async () => { const browser = await ${browserName}.launch({ - headless: false + ${launchOptions(browserChannel)} }); const context = await browser.newContext();`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); }); -it('should print the correct context options for custom settings', async ({ browserName, runCLI }) => { - const cli = runCLI(['codegen', '--color-scheme=light', emptyHTML]); +it('should print the correct context options for custom settings', async ({ browserName, browserChannel, runCLI }) => { + const cli = runCLI(['--color-scheme=light', emptyHTML]); const expectedResult = `const { ${browserName} } = require('playwright'); (async () => { const browser = await ${browserName}.launch({ - headless: false + ${launchOptions(browserChannel)} }); const context = await browser.newContext({ colorScheme: 'light' @@ -51,15 +55,15 @@ it('should print the correct context options for custom settings', async ({ brow }); -it('should print the correct context options when using a device', (test, { browserName }) => { +it('should print the correct context options when using a device', (test, { browserName, browserChannel }) => { test.skip(browserName !== 'chromium'); -}, async ({ runCLI }) => { - const cli = runCLI(['codegen', '--device=Pixel 2', emptyHTML]); +}, async ({ browserChannel, runCLI }) => { + const cli = runCLI(['--device=Pixel 2', emptyHTML]); const expectedResult = `const { chromium, devices } = require('playwright'); (async () => { const browser = await chromium.launch({ - headless: false + ${launchOptions(browserChannel)} }); const context = await browser.newContext({ ...devices['Pixel 2'], @@ -70,13 +74,13 @@ it('should print the correct context options when using a device', (test, { brow it('should print the correct context options when using a device and additional options', (test, { browserName }) => { test.skip(browserName !== 'webkit'); -}, async ({ runCLI }) => { - const cli = runCLI(['codegen', '--color-scheme=light', '--device=iPhone 11', emptyHTML]); +}, async ({ browserChannel, runCLI }) => { + const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', emptyHTML]); const expectedResult = `const { webkit, devices } = require('playwright'); (async () => { const browser = await webkit.launch({ - headless: false + ${launchOptions(browserChannel)} }); const context = await browser.newContext({ ...devices['iPhone 11'], @@ -86,16 +90,16 @@ it('should print the correct context options when using a device and additional expect(cli.text()).toContain(expectedResult); }); -it('should save the codegen output to a file if specified', async ({ browserName, runCLI, testInfo }) => { +it('should save the codegen output to a file if specified', async ({ browserName, browserChannel, runCLI, testInfo }) => { const tmpFile = testInfo.outputPath('script.js'); - const cli = runCLI(['codegen', '--output', tmpFile, emptyHTML]); + const cli = runCLI(['--output', tmpFile, emptyHTML]); await cli.exited; const content = fs.readFileSync(tmpFile); expect(content.toString()).toBe(`const { ${browserName} } = require('playwright'); (async () => { const browser = await ${browserName}.launch({ - headless: false + ${launchOptions(browserChannel)} }); const context = await browser.newContext(); @@ -114,16 +118,16 @@ it('should save the codegen output to a file if specified', async ({ browserName })();`); }); -it('should print load/save storageState', async ({ browserName, runCLI, testInfo }) => { +it('should print load/save storageState', async ({ browserName, browserChannel, runCLI, testInfo }) => { const loadFileName = testInfo.outputPath('load.json'); const saveFileName = testInfo.outputPath('save.json'); await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8'); - const cli = runCLI(['codegen', `--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, emptyHTML]); + const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, emptyHTML]); const expectedResult1 = `const { ${browserName} } = require('playwright'); (async () => { const browser = await ${browserName}.launch({ - headless: false + ${launchOptions(browserChannel)} }); const context = await browser.newContext({ storageState: '${loadFileName}' diff --git a/test/cli/cli-codegen-python-async.spec.ts b/test/cli/cli-codegen-python-async.spec.ts index f6933ffeda1ea..1b6a14c1d7b6b 100644 --- a/test/cli/cli-codegen-python-async.spec.ts +++ b/test/cli/cli-codegen-python-async.spec.ts @@ -21,26 +21,29 @@ import { folio } from './cli.fixtures'; const { it, expect } = folio; const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString(); +const launchOptions = (channel: string) => { + return channel ? `headless=False, channel="${channel}"` : 'headless=False'; +}; -it('should print the correct imports and context options', async ({ browserName, runCLI }) => { - const cli = runCLI(['codegen', '--target=python-async', emptyHTML]); +it('should print the correct imports and context options', async ({ browserName, browserChannel, runCLI }) => { + const cli = runCLI(['--target=python-async', emptyHTML]); const expectedResult = `import asyncio from playwright.async_api import async_playwright async def run(playwright): - browser = await playwright.${browserName}.launch(headless=False) + browser = await playwright.${browserName}.launch(${launchOptions(browserChannel)}) context = await browser.new_context()`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); }); -it('should print the correct context options for custom settings', async ({ browserName, runCLI }) => { - const cli = runCLI(['codegen', '--color-scheme=light', '--target=python-async', emptyHTML]); +it('should print the correct context options for custom settings', async ({ browserName, browserChannel, runCLI }) => { + const cli = runCLI(['--color-scheme=light', '--target=python-async', emptyHTML]); const expectedResult = `import asyncio from playwright.async_api import async_playwright async def run(playwright): - browser = await playwright.${browserName}.launch(headless=False) + browser = await playwright.${browserName}.launch(${launchOptions(browserChannel)}) context = await browser.new_context(color_scheme="light")`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); @@ -48,13 +51,13 @@ async def run(playwright): it('should print the correct context options when using a device', (test, { browserName }) => { test.skip(browserName !== 'chromium'); -}, async ({ runCLI }) => { - const cli = runCLI(['codegen', '--device=Pixel 2', '--target=python-async', emptyHTML]); +}, async ({ browserChannel, runCLI }) => { + const cli = runCLI(['--device=Pixel 2', '--target=python-async', emptyHTML]); const expectedResult = `import asyncio from playwright.async_api import async_playwright async def run(playwright): - browser = await playwright.chromium.launch(headless=False) + browser = await playwright.chromium.launch(${launchOptions(browserChannel)}) context = await browser.new_context(**playwright.devices["Pixel 2"])`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); @@ -62,28 +65,28 @@ async def run(playwright): it('should print the correct context options when using a device and additional options', (test, { browserName }) => { test.skip(browserName !== 'webkit'); -}, async ({ runCLI }) => { - const cli = runCLI(['codegen', '--color-scheme=light', '--device=iPhone 11', '--target=python-async', emptyHTML]); +}, async ({ browserChannel, runCLI }) => { + const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', '--target=python-async', emptyHTML]); const expectedResult = `import asyncio from playwright.async_api import async_playwright async def run(playwright): - browser = await playwright.webkit.launch(headless=False) + browser = await playwright.webkit.launch(${launchOptions(browserChannel)}) context = await browser.new_context(**playwright.devices["iPhone 11"], color_scheme="light")`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); }); -it('should save the codegen output to a file if specified', async ({ browserName, runCLI, testInfo }) => { +it('should save the codegen output to a file if specified', async ({ browserName, browserChannel, runCLI, testInfo }) => { const tmpFile = testInfo.outputPath('script.js'); - const cli = runCLI(['codegen', '--target=python-async', '--output', tmpFile, emptyHTML]); + const cli = runCLI(['--target=python-async', '--output', tmpFile, emptyHTML]); await cli.exited; const content = await fs.readFileSync(tmpFile); expect(content.toString()).toBe(`import asyncio from playwright.async_api import async_playwright async def run(playwright): - browser = await playwright.${browserName}.launch(headless=False) + browser = await playwright.${browserName}.launch(${launchOptions(browserChannel)}) context = await browser.new_context() # Open new page @@ -105,16 +108,16 @@ async def main(): asyncio.run(main())`); }); -it('should print load/save storage_state', async ({ browserName, runCLI, testInfo }) => { +it('should print load/save storage_state', async ({ browserName, browserChannel, runCLI, testInfo }) => { const loadFileName = testInfo.outputPath('load.json'); const saveFileName = testInfo.outputPath('save.json'); await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8'); - const cli = runCLI(['codegen', `--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=python-async', emptyHTML]); + const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=python-async', emptyHTML]); const expectedResult1 = `import asyncio from playwright.async_api import async_playwright async def run(playwright): - browser = await playwright.${browserName}.launch(headless=False) + browser = await playwright.${browserName}.launch(${launchOptions(browserChannel)}) context = await browser.new_context(storage_state="${loadFileName}")`; await cli.waitFor(expectedResult1); diff --git a/test/cli/cli-codegen-python.spec.ts b/test/cli/cli-codegen-python.spec.ts index 4671c5d26ac96..35932a8192b53 100644 --- a/test/cli/cli-codegen-python.spec.ts +++ b/test/cli/cli-codegen-python.spec.ts @@ -21,24 +21,27 @@ import { folio } from './cli.fixtures'; const { it, expect } = folio; const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString(); +const launchOptions = (channel: string) => { + return channel ? `headless=False, channel="${channel}"` : 'headless=False'; +}; -it('should print the correct imports and context options', async ({ runCLI, browserName }) => { - const cli = runCLI(['codegen', '--target=python', emptyHTML]); +it('should print the correct imports and context options', async ({ runCLI, browserChannel, browserName }) => { + const cli = runCLI(['--target=python', emptyHTML]); const expectedResult = `from playwright.sync_api import sync_playwright def run(playwright): - browser = playwright.${browserName}.launch(headless=False) + browser = playwright.${browserName}.launch(${launchOptions(browserChannel)}) context = browser.new_context()`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); }); -it('should print the correct context options for custom settings', async ({ runCLI, browserName }) => { - const cli = runCLI(['codegen', '--color-scheme=light', '--target=python', emptyHTML]); +it('should print the correct context options for custom settings', async ({ runCLI, browserChannel, browserName }) => { + const cli = runCLI(['--color-scheme=light', '--target=python', emptyHTML]); const expectedResult = `from playwright.sync_api import sync_playwright def run(playwright): - browser = playwright.${browserName}.launch(headless=False) + browser = playwright.${browserName}.launch(${launchOptions(browserChannel)}) context = browser.new_context(color_scheme="light")`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); @@ -46,12 +49,12 @@ def run(playwright): it('should print the correct context options when using a device', (test, { browserName }) => { test.skip(browserName !== 'chromium'); -}, async ({ runCLI }) => { - const cli = runCLI(['codegen', '--device=Pixel 2', '--target=python', emptyHTML]); +}, async ({ browserChannel, runCLI }) => { + const cli = runCLI(['--device=Pixel 2', '--target=python', emptyHTML]); const expectedResult = `from playwright.sync_api import sync_playwright def run(playwright): - browser = playwright.chromium.launch(headless=False) + browser = playwright.chromium.launch(${launchOptions(browserChannel)}) context = browser.new_context(**playwright.devices["Pixel 2"])`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); @@ -59,26 +62,26 @@ def run(playwright): it('should print the correct context options when using a device and additional options', (test, { browserName }) => { test.skip(browserName !== 'webkit'); -}, async ({ runCLI }) => { - const cli = runCLI(['codegen', '--color-scheme=light', '--device=iPhone 11', '--target=python', emptyHTML]); +}, async ({ browserChannel, runCLI }) => { + const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', '--target=python', emptyHTML]); const expectedResult = `from playwright.sync_api import sync_playwright def run(playwright): - browser = playwright.webkit.launch(headless=False) + browser = playwright.webkit.launch(${launchOptions(browserChannel)}) context = browser.new_context(**playwright.devices["iPhone 11"], color_scheme="light")`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); }); -it('should save the codegen output to a file if specified', async ({ runCLI, browserName, testInfo }) => { +it('should save the codegen output to a file if specified', async ({ runCLI, browserChannel, browserName, testInfo }) => { const tmpFile = testInfo.outputPath('script.js'); - const cli = runCLI(['codegen', '--target=python', '--output', tmpFile, emptyHTML]); + const cli = runCLI(['--target=python', '--output', tmpFile, emptyHTML]); await cli.exited; const content = fs.readFileSync(tmpFile); expect(content.toString()).toBe(`from playwright.sync_api import sync_playwright def run(playwright): - browser = playwright.${browserName}.launch(headless=False) + browser = playwright.${browserName}.launch(${launchOptions(browserChannel)}) context = browser.new_context() # Open new page @@ -98,15 +101,15 @@ with sync_playwright() as playwright: run(playwright)`); }); -it('should print load/save storage_state', async ({ runCLI, browserName, testInfo }) => { +it('should print load/save storage_state', async ({ runCLI, browserChannel, browserName, testInfo }) => { const loadFileName = testInfo.outputPath('load.json'); const saveFileName = testInfo.outputPath('save.json'); await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8'); - const cli = runCLI(['codegen', `--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=python', emptyHTML]); + const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=python', emptyHTML]); const expectedResult1 = `from playwright.sync_api import sync_playwright def run(playwright): - browser = playwright.${browserName}.launch(headless=False) + browser = playwright.${browserName}.launch(${launchOptions(browserChannel)}) context = browser.new_context(storage_state="${loadFileName}")`; await cli.waitFor(expectedResult1); diff --git a/test/cli/cli.fixtures.ts b/test/cli/cli.fixtures.ts index e22ec96dc1595..cefcfdb57a5c7 100644 --- a/test/cli/cli.fixtures.ts +++ b/test/cli/cli.fixtures.ts @@ -149,10 +149,10 @@ class Recorder { } } -fixtures.runCLI.init(async ({ browserName, headful }, runTest) => { +fixtures.runCLI.init(async ({ browserName, browserChannel, headful }, runTest) => { let cli: CLIMock; const cliFactory = (args: string[]) => { - cli = new CLIMock(browserName, !headful, args); + cli = new CLIMock(browserName, browserChannel, !headful, args); return cli; }; await runTest(cliFactory); @@ -166,13 +166,17 @@ class CLIMock { private waitForCallback: () => void; exited: Promise; - constructor(browserName: string, headless: boolean, args: string[]) { + constructor(browserName: string, browserChannel: string, headless: boolean, args: string[]) { this.data = ''; - this.process = spawn('node', [ + const nodeArgs = [ path.join(__dirname, '..', '..', 'lib', 'cli', 'cli.js'), + 'codegen', ...args, `--browser=${browserName}`, - ], { + ]; + if (browserChannel) + nodeArgs.push(`--channel=${browserChannel}`); + this.process = spawn('node', nodeArgs, { env: { ...process.env, PWCLI_EXIT_FOR_TEST: '1', diff --git a/test/downloads-path.spec.ts b/test/downloads-path.spec.ts index 64f41e7dd843b..cc398b30f13a0 100644 --- a/test/downloads-path.spec.ts +++ b/test/downloads-path.spec.ts @@ -39,7 +39,7 @@ fixtures.downloadsBrowser.init(async ({ server, browserType, browserOptions, tes await browser.close(); }); -fixtures.persistentDownloadsContext.init(async ({ server, launchPersistent, testInfo }, test) => { +fixtures.persistentDownloadsContext.init(async ({ server, launchPersistent, testInfo, browserChannel }, test) => { server.setRoute('/download', (req, res) => { res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Disposition', 'attachment; filename=file.txt'); @@ -49,7 +49,8 @@ fixtures.persistentDownloadsContext.init(async ({ server, launchPersistent, test const { context, page } = await launchPersistent( { downloadsPath: testInfo.outputPath(''), - acceptDownloads: true + acceptDownloads: true, + channel: browserChannel, } ); logOnCI('--- setting content for the page ---'); diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts index 78c0bb05a8bff..86df243cf137f 100644 --- a/test/fixtures.spec.ts +++ b/test/fixtures.spec.ts @@ -80,21 +80,27 @@ describe('fixtures', (suite, { platform, headful }) => { await connectedRemoteServer.childExitCode(); }); - it('should close the browser on SIGINT', async ({connectedRemoteServer}) => { + it('should close the browser on SIGINT', (test, { browserChannel }) => { + test.fixme(!!browserChannel, 'Uncomment on roll'); + }, async ({connectedRemoteServer}) => { process.kill(connectedRemoteServer.child().pid, 'SIGINT'); expect(await connectedRemoteServer.out('exitCode')).toBe('0'); expect(await connectedRemoteServer.out('signal')).toBe('null'); expect(await connectedRemoteServer.childExitCode()).toBe(130); }); - it('should close the browser on SIGTERM', async ({connectedRemoteServer}) => { + it('should close the browser on SIGTERM', (test, { browserChannel }) => { + test.fixme(!!browserChannel, 'Uncomment on roll'); + }, async ({connectedRemoteServer}) => { process.kill(connectedRemoteServer.child().pid, 'SIGTERM'); expect(await connectedRemoteServer.out('exitCode')).toBe('0'); expect(await connectedRemoteServer.out('signal')).toBe('null'); expect(await connectedRemoteServer.childExitCode()).toBe(0); }); - it('should close the browser on SIGHUP', async ({connectedRemoteServer}) => { + it('should close the browser on SIGHUP', (test, { browserChannel }) => { + test.fixme(!!browserChannel, 'Uncomment on roll'); + }, async ({connectedRemoteServer}) => { process.kill(connectedRemoteServer.child().pid, 'SIGHUP'); expect(await connectedRemoteServer.out('exitCode')).toBe('0'); expect(await connectedRemoteServer.out('signal')).toBe('null'); diff --git a/test/fixtures.ts b/test/fixtures.ts index e2d7acb32012a..fd7dde40fa32b 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -87,12 +87,12 @@ fixtures.launchPersistent.init(async ({ createUserDataDir, browserOptions, brows await context.close(); }); -fixtures.browserOptions.override(async ({ browserName, headful, slowMo }, run) => { +fixtures.browserOptions.override(async ({ browserName, headful, slowMo, browserChannel }, run) => { const executablePath = getExecutablePath(browserName); if (executablePath) console.error(`Using executable at ${executablePath}`); await run({ - channel: process.env.PW_CHROMIUM_CHANNEL, + channel: browserChannel, executablePath, handleSIGINT: false, slowMo, diff --git a/test/playwright.fixtures.ts b/test/playwright.fixtures.ts index 9235ddfb06e67..7b2bcdcbd7c62 100644 --- a/test/playwright.fixtures.ts +++ b/test/playwright.fixtures.ts @@ -34,6 +34,8 @@ config.timeout = 30000; type PlaywrightParameters = { // Browser type name. browserName: 'chromium' | 'firefox' | 'webkit'; + // Browser release channel, if applicable. + browserChannel: string | undefined; // Whether to run tests headless or headful. headful: boolean; // Operating system. @@ -94,12 +96,14 @@ fixtures.platform.initParameter('Operating system', process.platform as ('win32' fixtures.screenshotOnFailure.initParameter('Generate screenshot on failure', false); fixtures.slowMo.initParameter('Slows down Playwright operations by the specified amount of milliseconds', 0); fixtures.video.initParameter('Record videos while running tests', false); +fixtures.browserChannel.initParameter('Browser release channel', process.env.PW_CHROMIUM_CHANNEL); -fixtures.browserOptions.init(async ({ headful, slowMo }, run) => { +fixtures.browserOptions.init(async ({ headful, slowMo, browserChannel }, run) => { await run({ handleSIGINT: false, slowMo, headless: !headful, + channel: browserChannel, }); }, { scope: 'worker' }); diff --git a/test/remoteServer.fixture.ts b/test/remoteServer.fixture.ts index c9946ac27e9f1..67ba1b8a50303 100644 --- a/test/remoteServer.fixture.ts +++ b/test/remoteServer.fixture.ts @@ -69,11 +69,12 @@ export class RemoteServer { this._didExit = false; this._browserType = browserType; - const launchOptions = {...browserOptions, + const launchOptions = { + ...browserOptions, handleSIGINT: true, handleSIGTERM: true, handleSIGHUP: true, - executablePath: browserOptions.executablePath || browserType.executablePath(), + executablePath: browserOptions.channel ? undefined : browserOptions.executablePath || browserType.executablePath(), logger: undefined, }; const options = { diff --git a/test/screencast.spec.ts b/test/screencast.spec.ts index e29df7bbeca9b..356913cc89e0b 100644 --- a/test/screencast.spec.ts +++ b/test/screencast.spec.ts @@ -415,7 +415,7 @@ describe('screencast', suite => { expect(videoPlayer.videoHeight).toBe(600); }); - it('should be 800x450 by default', async ({browser, testInfo}) => { + it('should be 800x450 by default', async ({ browser, testInfo }) => { const context = await browser.newContext({ recordVideo: { dir: testInfo.outputPath(''), diff --git a/types/types.d.ts b/types/types.d.ts index db58257b77aae..267a0e3889e52 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -6356,6 +6356,19 @@ export interface BrowserType { */ bypassCSP?: boolean; + /** + * Chromium distribution channel, one of + * - chrome + * - chrome-beta + * - chrome-dev + * - chrome-canary + * - msedge + * - msedge-beta + * - msedge-dev + * - msedge-canary + */ + channel?: string; + /** * Enable Chromium sandboxing. Defaults to `true`. */