From fda9051c750ba43b9360767443b6bd5c1f40ef45 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 15 May 2024 02:34:39 -0700 Subject: [PATCH 01/32] feat(webkit): roll to r2008 (#30818) Signed-off-by: Max Schmitt Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Max Schmitt --- packages/playwright-core/browsers.json | 2 +- tests/library/capabilities.spec.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 847202d036..e81195c8c0 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2007", + "revision": "2008", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/tests/library/capabilities.spec.ts b/tests/library/capabilities.spec.ts index d713260f5e..fbeeeb65be 100644 --- a/tests/library/capabilities.spec.ts +++ b/tests/library/capabilities.spec.ts @@ -32,7 +32,8 @@ it('SharedArrayBuffer should work @smoke', async function({ contextFactory, http expect(await page.evaluate(() => typeof SharedArrayBuffer)).toBe('function'); }); -it('Web Assembly should work @smoke', async function({ page, server }) { +it('Web Assembly should work @smoke', async ({ page, server, browserName, platform }) => { + it.fixme(browserName === 'webkit' && platform === 'win32', 'Windows JIT is disabled: https://bugs.webkit.org/show_bug.cgi?id=273854'); await page.goto(server.PREFIX + '/wasm/table2.html'); expect(await page.evaluate('loadTable()')).toBe('42, 83'); }); From 7a588e6c720a3f419d3f4f5113acec810643fdbf Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 15 May 2024 09:05:06 -0700 Subject: [PATCH 02/32] chore: do not close the reused context when video is on (#30807) Fixes https://github.com/microsoft/playwright/issues/30779 --- packages/playwright/src/common/config.ts | 2 +- packages/playwright/src/index.ts | 4 +-- packages/playwright/src/runner/testServer.ts | 1 + .../playwright-test/playwright.reuse.spec.ts | 29 ------------------- 4 files changed, 4 insertions(+), 32 deletions(-) diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 7bc56cf4f6..d54c739726 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -265,7 +265,7 @@ export function toReporters(reporters: BuiltInReporter | ReporterDescription[] | export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob', 'markdown'] as const; export type BuiltInReporter = typeof builtInReporters[number]; -export type ContextReuseMode = 'none' | 'force' | 'when-possible'; +export type ContextReuseMode = 'none' | 'when-possible'; function resolveScript(id: string | undefined, rootDir: string): string | undefined { if (!id) diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 9b8b4bc55b..0a6651f362 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -356,8 +356,8 @@ const playwrightFixtures: Fixtures = ({ _reuseContext: [async ({ video, _optionContextReuseMode }, use) => { let mode = _optionContextReuseMode; if (process.env.PW_TEST_REUSE_CONTEXT) - mode = process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : (process.env.PW_TEST_REUSE_CONTEXT ? 'force' : 'none'); - const reuse = mode === 'force' || (mode === 'when-possible' && normalizeVideoMode(video) === 'off'); + mode = 'when-possible'; + const reuse = mode === 'when-possible' && normalizeVideoMode(video) === 'off'; await use(reuse); }, { scope: 'worker', _title: 'context' } as any], diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 0809bd43d7..e7670c3548 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -308,6 +308,7 @@ class TestServerDispatcher implements TestServerInterface { reporter: params.reporters ? params.reporters.map(r => [r]) : undefined, use: { trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : (params.trace === 'off' ? 'off' : undefined), + video: 'off', headless: params.headed ? false : undefined, _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined, _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined, diff --git a/tests/playwright-test/playwright.reuse.spec.ts b/tests/playwright-test/playwright.reuse.spec.ts index 799b6fa3d7..0620195f65 100644 --- a/tests/playwright-test/playwright.reuse.spec.ts +++ b/tests/playwright-test/playwright.reuse.spec.ts @@ -85,35 +85,6 @@ test('should not reuse context with video if mode=when-possible', async ({ runIn expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'video.webm'))).toBeFalsy(); }); -test('should reuse context and disable video if mode=force', async ({ runInlineTest }, testInfo) => { - const result = await runInlineTest({ - 'playwright.config.ts': ` - export default { - use: { video: 'on' }, - }; - `, - 'reuse.test.ts': ` - import { test, expect } from '@playwright/test'; - let lastContextGuid; - - test('one', async ({ context, page }) => { - lastContextGuid = context._guid; - await page.waitForTimeout(2000); - }); - - test('two', async ({ context, page }) => { - expect(context._guid).toBe(lastContextGuid); - await page.waitForTimeout(2000); - }); - `, - }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); - expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'video.webm'))).toBeFalsy(); - expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'video.webm'))).toBeFalsy(); -}); - test('should reuse context with trace if mode=when-possible', async ({ runInlineTest }, testInfo) => { const result = await runInlineTest({ 'playwright.config.ts': ` From 6ae5cd382458f344767fd1ab96fa331a384c5e79 Mon Sep 17 00:00:00 2001 From: Joe-Hendley <95080839+Joe-Hendley@users.noreply.github.com> Date: Wed, 15 May 2024 17:10:10 +0100 Subject: [PATCH 03/32] feat: implement flag to fail flaky tests (#30618) Implements feature requested in https://github.com/microsoft/playwright/issues/30457 The test runner treats flaky tests as failures when the flag is enabled, but still reports flaky tests as flaky in the reporting interface. It feels like something worth discussing as this behaviour makes sense to me, but looked a bit odd to @BJSS-russell-pollock when I ran this past him. Closes #30457. --- docs/src/test-cli-js.md | 1 + packages/playwright/src/common/config.ts | 1 + packages/playwright/src/program.ts | 2 ++ packages/playwright/src/runner/failureTracker.ts | 10 +++++++++- tests/playwright-test/exit-code.spec.ts | 13 +++++++++++++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/src/test-cli-js.md b/docs/src/test-cli-js.md index 6bfb213454..4f012962fc 100644 --- a/docs/src/test-cli-js.md +++ b/docs/src/test-cli-js.md @@ -81,6 +81,7 @@ Complete set of Playwright Test options is available in the [configuration file] | Non-option arguments | Each argument is treated as a regular expression matched against the full test file path. Only tests from the files matching the pattern will be executed. Special symbols like `$` or `*` should be escaped with `\`. In many shells/terminals you may need to quote the arguments. | | `-c ` or `--config `| Configuration file. If not passed, defaults to `playwright.config.ts` or `playwright.config.js` in the current directory. | | `--debug`| Run tests with Playwright Inspector. Shortcut for `PWDEBUG=1` environment variable and `--timeout=0 --max-failures=1 --headed --workers=1` options.| +| `--fail-on-flaky-tests` | Fails test runs that contain flaky tests. By default flaky tests count as successes. | | `--forbid-only` | Whether to disallow `test.only`. Useful on CI.| | `--global-timeout ` | Total timeout for the whole test run in milliseconds. By default, there is no global timeout. Learn more about [various timeouts](./test-timeouts.md).| | `-g ` or `--grep ` | Only run tests matching this regular expression. For example, this will run `'should add to cart'` when passed `-g "add to cart"`. The regular expression will be tested against the string that consists of the test file name, `test.describe` titles if any, test title and all test tags, separated by spaces, e.g. `my-test.spec.ts my-suite my-test @smoke`. The filter does not apply to the tests from dependency projects, i.e. Playwright will still run all tests from [project dependencies](./test-projects.md#dependencies). | diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index d54c739726..42dd554b38 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -52,6 +52,7 @@ export class FullConfigInternal { cliProjectFilter?: string[]; cliListOnly = false; cliPassWithNoTests?: boolean; + cliFailOnFlakyTests?: boolean; testIdMatcher?: Matcher; defineConfigWasUsed = false; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index feca8d644b..b69d3d6b10 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -194,6 +194,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { config.cliListOnly = !!opts.list; config.cliProjectFilter = opts.project || undefined; config.cliPassWithNoTests = !!opts.passWithNoTests; + config.cliFailOnFlakyTests = !!opts.failOnFlakyTests; const runner = new Runner(config); let status: FullResult['status']; @@ -336,6 +337,7 @@ const testOptions: [string, string][] = [ ['--browser ', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`], ['-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`], ['--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options`], + ['--fail-on-flaky-tests', `Fail if any test is flagged as flaky (default: false)`], ['--forbid-only', `Fail if test.only is called (default: false)`], ['--fully-parallel', `Run all tests in parallel (default: false)`], ['--global-timeout ', `Maximum time this test suite can run in milliseconds (default: unlimited)`], diff --git a/packages/playwright/src/runner/failureTracker.ts b/packages/playwright/src/runner/failureTracker.ts index 702ea668c2..6ea8f81a34 100644 --- a/packages/playwright/src/runner/failureTracker.ts +++ b/packages/playwright/src/runner/failureTracker.ts @@ -48,7 +48,15 @@ export class FailureTracker { } result(): 'failed' | 'passed' { - return this._hasWorkerErrors || this.hasReachedMaxFailures() || this._rootSuite?.allTests().some(test => !test.ok()) ? 'failed' : 'passed'; + return this._hasWorkerErrors || this.hasReachedMaxFailures() || this.hasFailedTests() || (this._config.cliFailOnFlakyTests && this.hasFlakyTests()) ? 'failed' : 'passed'; + } + + hasFailedTests() { + return this._rootSuite?.allTests().some(test => !test.ok()); + } + + hasFlakyTests() { + return this._rootSuite?.allTests().some(test => (test.outcome() === 'flaky')); } maxFailures() { diff --git a/tests/playwright-test/exit-code.spec.ts b/tests/playwright-test/exit-code.spec.ts index 661385c950..e5b1165f07 100644 --- a/tests/playwright-test/exit-code.spec.ts +++ b/tests/playwright-test/exit-code.spec.ts @@ -128,6 +128,19 @@ test('should allow flaky', async ({ runInlineTest }) => { expect(result.flaky).toBe(1); }); +test('failOnFlakyTests flag disallows flaky', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('flake', async ({}, testInfo) => { + expect(testInfo.retry).toBe(1); + }); + `, + }, { 'retries': 1, 'fail-on-flaky-tests': true }); + expect(result.exitCode).not.toBe(0); + expect(result.flaky).toBe(1); +}); + test('should fail on unexpected pass', async ({ runInlineTest }) => { const { exitCode, failed, output } = await runInlineTest({ 'unexpected-pass.spec.js': ` From 90765a226f2e86757beb8b0bc7d168c8237a7db0 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 15 May 2024 18:20:00 +0200 Subject: [PATCH 04/32] fix(electron): allow launching with spaces in path (#30820) Fixes https://github.com/microsoft/playwright/issues/30755 --- .../src/server/electron/electron.ts | 16 ++++++++++++---- .../playwright-electron-should-work.spec.ts | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index c55257510c..9b934ca2f7 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -161,7 +161,7 @@ export class Electron extends SdkObject { return controller.run(async progress => { let app: ElectronApplication | undefined = undefined; // --remote-debugging-port=0 must be the last playwright's argument, loader.ts relies on it. - const electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...args]; + let electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...args]; if (os.platform() === 'linux') { const runningAsRoot = process.geteuid && process.geteuid() === 0; @@ -195,6 +195,16 @@ export class Electron extends SdkObject { // Packaged apps might have their own command line handling. electronArguments.unshift('-r', require.resolve('./loader')); } + let shell = false; + if (process.platform === 'win32') { + // On Windows in order to run .cmd files, shell: true is required. + // https://github.com/nodejs/node/issues/52554 + shell = true; + // On Windows, we need to quote the executable path due to shell: true. + command = `"${command}"`; + // On Windows, we need to quote the arguments due to shell: true. + electronArguments = electronArguments.map(arg => `"${arg}"`); + } // When debugging Playwright test that runs Electron, NODE_OPTIONS // will make the debugger attach to Electron's Node. But Playwright @@ -208,9 +218,7 @@ export class Electron extends SdkObject { progress.log(message); browserLogsCollector.log(message); }, - // On Windows in order to run .cmd files, shell: true is required. - // https://github.com/nodejs/node/issues/52554 - shell: process.platform === 'win32', + shell, stdio: 'pipe', cwd: options.cwd, tempDirectories: [artifactsDir], diff --git a/tests/installation/playwright-electron-should-work.spec.ts b/tests/installation/playwright-electron-should-work.spec.ts index 76cad12ec7..4de28e8b54 100755 --- a/tests/installation/playwright-electron-should-work.spec.ts +++ b/tests/installation/playwright-electron-should-work.spec.ts @@ -14,6 +14,8 @@ * limitations under the License. */ import { test } from './npmTest'; +import fs from 'fs'; +import path from 'path'; test('electron should work', async ({ exec, tsc, writeFiles }) => { await exec('npm i playwright electron@19.0.11'); @@ -24,3 +26,16 @@ test('electron should work', async ({ exec, tsc, writeFiles }) => { }); await tsc('test.ts'); }); + +test('electron should work with special characters in path', async ({ exec, tmpWorkspace }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30755' }); + const folderName = path.join(tmpWorkspace, '!@#$% тест with spaces and 😊'); + + await exec('npm i playwright electron@19.0.11'); + await fs.promises.mkdir(folderName); + for (const file of ['electron-app.js', 'sanity-electron.js']) + await fs.promises.copyFile(path.join(tmpWorkspace, file), path.join(folderName, file)); + await exec('node sanity-electron.js', { + cwd: path.join(folderName) + }); +}); From 5fa0583dcb708e74d2f7fc456b8c44cec9752709 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 15 May 2024 10:37:36 -0700 Subject: [PATCH 05/32] fix(test runner): regular worker termination finishes long fixtures (#30769) Previously, terminating worker always had a 30 seconds force exit. Now, regular worker termination assumes that process will eventually finish tearing down all the fixtures and exits. However, the self-destruction routine keeps the 30 seconds timeout to avoid zombies. Fixes #30504. --- packages/playwright/src/common/process.ts | 38 ++++++++++------- packages/playwright/src/worker/workerMain.ts | 20 ++++----- tests/playwright-test/timeout.spec.ts | 44 ++++++++++++++++++++ 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/packages/playwright/src/common/process.ts b/packages/playwright/src/common/process.ts index 4e1e2672ff..14ad995fed 100644 --- a/packages/playwright/src/common/process.ts +++ b/packages/playwright/src/common/process.ts @@ -44,11 +44,12 @@ export class ProcessRunner { } } -let closed = false; +let gracefullyCloseCalled = false; +let forceExitInitiated = false; sendMessageToParent({ method: 'ready' }); -process.on('disconnect', gracefullyCloseAndExit); +process.on('disconnect', () => gracefullyCloseAndExit(true)); process.on('SIGINT', () => {}); process.on('SIGTERM', () => {}); @@ -76,7 +77,7 @@ process.on('message', async (message: any) => { const keys = new Set([...Object.keys(process.env), ...Object.keys(startingEnv)]); const producedEnv: EnvProducedPayload = [...keys].filter(key => startingEnv[key] !== process.env[key]).map(key => [key, process.env[key] ?? null]); sendMessageToParent({ method: '__env_produced__', params: producedEnv }); - await gracefullyCloseAndExit(); + await gracefullyCloseAndExit(false); return; } if (message.method === '__dispatch__') { @@ -92,19 +93,24 @@ process.on('message', async (message: any) => { } }); -async function gracefullyCloseAndExit() { - if (closed) - return; - closed = true; - // Force exit after 30 seconds. - // eslint-disable-next-line no-restricted-properties - setTimeout(() => process.exit(0), 30000); - // Meanwhile, try to gracefully shutdown. - await processRunner?.gracefullyClose().catch(() => {}); - if (processName) - await stopProfiling(processName).catch(() => {}); - // eslint-disable-next-line no-restricted-properties - process.exit(0); +const kForceExitTimeout = +(process.env.PWTEST_FORCE_EXIT_TIMEOUT || 30000); + +async function gracefullyCloseAndExit(forceExit: boolean) { + if (forceExit && !forceExitInitiated) { + forceExitInitiated = true; + // Force exit after 30 seconds. + // eslint-disable-next-line no-restricted-properties + setTimeout(() => process.exit(0), kForceExitTimeout); + } + if (!gracefullyCloseCalled) { + gracefullyCloseCalled = true; + // Meanwhile, try to gracefully shutdown. + await processRunner?.gracefullyClose().catch(() => {}); + if (processName) + await stopProfiling(processName).catch(() => {}); + // eslint-disable-next-line no-restricted-properties + process.exit(0); + } } function sendMessageToParent(message: { method: string, params?: any }) { diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index 9ae9a86207..ea2cdbaebc 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -100,12 +100,17 @@ export class WorkerMain extends ProcessRunner { override async gracefullyClose() { try { await this._stop(); + // Ignore top-level errors, they are already inside TestInfo.errors. + const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {}); + const runnable = { type: 'teardown' } as const; // We have to load the project to get the right deadline below. - await this._loadIfNeeded(); - await this._teardownScopes(); + await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => this._loadIfNeeded()).catch(() => {}); + await this._fixtureRunner.teardownScope('test', fakeTestInfo, runnable).catch(() => {}); + await this._fixtureRunner.teardownScope('worker', fakeTestInfo, runnable).catch(() => {}); // Close any other browsers launched in this process. This includes anything launched // manually in the test/hooks and internal browsers like Playwright Inspector. - await gracefullyCloseAll(); + await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {}); + this._fatalErrors.push(...fakeTestInfo.errors); } catch (e) { this._fatalErrors.push(serializeError(e)); } @@ -144,15 +149,6 @@ export class WorkerMain extends ProcessRunner { } } - private async _teardownScopes() { - const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {}); - const runnable = { type: 'teardown' } as const; - // Ignore top-level errors, they are already inside TestInfo.errors. - await this._fixtureRunner.teardownScope('test', fakeTestInfo, runnable).catch(() => {}); - await this._fixtureRunner.teardownScope('worker', fakeTestInfo, runnable).catch(() => {}); - this._fatalErrors.push(...fakeTestInfo.errors); - } - unhandledError(error: Error | any) { // No current test - fatal error. if (!this._currentTest) { diff --git a/tests/playwright-test/timeout.spec.ts b/tests/playwright-test/timeout.spec.ts index 0ffa2164ce..50069b3280 100644 --- a/tests/playwright-test/timeout.spec.ts +++ b/tests/playwright-test/timeout.spec.ts @@ -514,3 +514,47 @@ test('should report up to 3 timeout errors', async ({ runInlineTest }) => { expect(result.output).toContain('Test timeout of 1000ms exceeded while running "afterEach" hook.'); expect(result.output).toContain('Worker teardown timeout of 1000ms exceeded while tearing down "autoWorker".'); }); + +test('should complain when worker fixture times out during worker cleanup', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test as base, expect } from '@playwright/test'; + const test = base.extend({ + slowTeardown: [async ({}, use) => { + await use('hey'); + await new Promise(f => setTimeout(f, 2000)); + }, { scope: 'worker', auto: true, timeout: 400 }], + }); + test('test ok', async ({ slowTeardown }) => { + expect(slowTeardown).toBe('hey'); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(1); + expect(result.output).toContain(`Fixture "slowTeardown" timeout of 400ms exceeded during teardown.`); +}); + +test('should allow custom worker fixture timeout longer than force exit cap', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test as base, expect } from '@playwright/test'; + const test = base.extend({ + slowTeardown: [async ({}, use) => { + await use('hey'); + await new Promise(f => setTimeout(f, 1500)); + console.log('output from teardown'); + throw new Error('Oh my!'); + }, { scope: 'worker', auto: true, timeout: 2000 }], + }); + test('test ok', async ({ slowTeardown }) => { + expect(slowTeardown).toBe('hey'); + }); + ` + }, {}, { PWTEST_FORCE_EXIT_TIMEOUT: '400' }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(1); + expect(result.output).toContain(`output from teardown`); + expect(result.output).toContain(`Error: Oh my!`); + expect(result.output).toContain(`1 error was not a part of any test, see above for details`); +}); From 8dec672121bb12dbc8371995c1cdba3ca0565ffb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 15 May 2024 12:45:57 -0700 Subject: [PATCH 06/32] chore(testServer): accept video parameter when running tests (#30832) --- packages/playwright/src/isomorphic/testServerInterface.ts | 1 + packages/playwright/src/runner/testServer.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 56cac53f5b..4abf3f02fe 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -94,6 +94,7 @@ export interface TestServerInterface { timeout?: number, reporters?: string[], trace?: 'on' | 'off'; + video?: 'on' | 'off'; projects?: string[]; reuseContext?: boolean; connectWsEndpoint?: string; diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index e7670c3548..706e296da4 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -308,7 +308,7 @@ class TestServerDispatcher implements TestServerInterface { reporter: params.reporters ? params.reporters.map(r => [r]) : undefined, use: { trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : (params.trace === 'off' ? 'off' : undefined), - video: 'off', + video: params.video === 'on' ? 'on' : (params.video === 'off' ? 'off' : undefined), headless: params.headed ? false : undefined, _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined, _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined, From 3370f37e7b1b0153feb086a744d1abdef47c1246 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 15 May 2024 14:24:42 -0700 Subject: [PATCH 07/32] feat(webkit): roll to r2009 (#30833) --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index e81195c8c0..45651d1a11 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2008", + "revision": "2009", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 89cdf3d56e01413dd7673bd37a2ef2aff5aff883 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 15 May 2024 15:01:52 -0700 Subject: [PATCH 08/32] feat: env.PLAYWRIGHT_FORCE_TTY to control reporters' tty (#30834) Previously, terminal reporters consulted `process.stdout.isTTY`. Now it is possible to control the tty behavior: - `PLAYWRIGHT_FORCE_TTY=0` or `PLAYWRIGHT_FORCE_TTY=false` to disable TTY; - `PLAYWRIGHT_FORCE_TTY=1` or `PLAYWRIGHT_FORCE_TTY=true` to enable TTY, defaults to 100 columns when real columns are unavailable; - `PLAYWRIGHT_FORCE_TTY=` to force enable TTY and set the columns. Fixes #29422. --- packages/playwright/src/reporters/base.ts | 54 +++++++++++++-------- packages/playwright/src/reporters/list.ts | 2 - tests/playwright-test/reporter-blob.spec.ts | 2 +- tests/playwright-test/reporter-list.spec.ts | 14 +++--- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 0c023e8761..f418baeea4 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -45,27 +45,41 @@ type TestSummary = { fatalErrors: TestError[]; }; -export const isTTY = !!process.env.PWTEST_TTY_WIDTH || process.stdout.isTTY; -export const ttyWidth = process.env.PWTEST_TTY_WIDTH ? parseInt(process.env.PWTEST_TTY_WIDTH, 10) : process.stdout.columns || 0; -let useColors = isTTY; -if (process.env.DEBUG_COLORS === '0' - || process.env.DEBUG_COLORS === 'false' - || process.env.FORCE_COLOR === '0' - || process.env.FORCE_COLOR === 'false') - useColors = false; -else if (process.env.DEBUG_COLORS || process.env.FORCE_COLOR) - useColors = true; +export const { isTTY, ttyWidth, colors } = (() => { + let isTTY = !!process.stdout.isTTY; + let ttyWidth = process.stdout.columns || 0; + if (process.env.PLAYWRIGHT_FORCE_TTY === 'false' || process.env.PLAYWRIGHT_FORCE_TTY === '0') { + isTTY = false; + ttyWidth = 0; + } else if (process.env.PLAYWRIGHT_FORCE_TTY === 'true' || process.env.PLAYWRIGHT_FORCE_TTY === '1') { + isTTY = true; + ttyWidth = process.stdout.columns || 100; + } else if (process.env.PLAYWRIGHT_FORCE_TTY) { + isTTY = true; + ttyWidth = +process.env.PLAYWRIGHT_FORCE_TTY; + if (isNaN(ttyWidth)) + ttyWidth = 100; + } -export const colors = useColors ? realColors : { - bold: (t: string) => t, - cyan: (t: string) => t, - dim: (t: string) => t, - gray: (t: string) => t, - green: (t: string) => t, - red: (t: string) => t, - yellow: (t: string) => t, - enabled: false, -}; + let useColors = isTTY; + if (process.env.DEBUG_COLORS === '0' || process.env.DEBUG_COLORS === 'false' || + process.env.FORCE_COLOR === '0' || process.env.FORCE_COLOR === 'false') + useColors = false; + else if (process.env.DEBUG_COLORS || process.env.FORCE_COLOR) + useColors = true; + + const colors = useColors ? realColors : { + bold: (t: string) => t, + cyan: (t: string) => t, + dim: (t: string) => t, + gray: (t: string) => t, + green: (t: string) => t, + red: (t: string) => t, + yellow: (t: string) => t, + enabled: false, + }; + return { isTTY, ttyWidth, colors }; +})(); export class BaseReporter implements ReporterV2 { config!: FullConfig; diff --git a/packages/playwright/src/reporters/list.ts b/packages/playwright/src/reporters/list.ts index 11f6a8b755..65057e16f9 100644 --- a/packages/playwright/src/reporters/list.ts +++ b/packages/playwright/src/reporters/list.ts @@ -229,8 +229,6 @@ class ListReporter extends BaseReporter { // Go down if needed. if (row !== this._lastRow) process.stdout.write(`\u001B[${this._lastRow - row}E`); - if (process.env.PWTEST_TTY_WIDTH) - process.stdout.write('\n'); // For testing. } private _testPrefix(index: string, statusMark: string) { diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 2e0e39a337..dbd91049d1 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -441,7 +441,7 @@ test('merge into list report by default', async ({ runInlineTest, mergeReports } const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip', 'report-3.zip']); - const { exitCode, output } = await mergeReports(reportDir, { PW_TEST_DEBUG_REPORTERS: '1', PW_TEST_DEBUG_REPORTERS_PRINT_STEPS: '1', PWTEST_TTY_WIDTH: '80' }, { additionalArgs: ['--reporter', 'list'] }); + const { exitCode, output } = await mergeReports(reportDir, { PW_TEST_DEBUG_REPORTERS: '1', PW_TEST_DEBUG_REPORTERS_PRINT_STEPS: '1', PLAYWRIGHT_FORCE_TTY: '80' }, { additionalArgs: ['--reporter', 'list'] }); expect(exitCode).toBe(0); const text = stripAnsi(output); diff --git a/tests/playwright-test/reporter-list.spec.ts b/tests/playwright-test/reporter-list.spec.ts index 245b0e26d3..2b924e5986 100644 --- a/tests/playwright-test/reporter-list.spec.ts +++ b/tests/playwright-test/reporter-list.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { test, expect } from './playwright-test-fixtures'; +import { test, expect, stripAnsi } from './playwright-test-fixtures'; const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION; const POSITIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'ok' : '✓ '; @@ -70,7 +70,7 @@ for (const useIntermediateMergeReport of [false, true] as const) { }); }); `, - }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PW_TEST_DEBUG_REPORTERS_PRINT_STEPS: '1', PWTEST_TTY_WIDTH: '80' }); + }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PW_TEST_DEBUG_REPORTERS_PRINT_STEPS: '1', PLAYWRIGHT_FORCE_TTY: '80' }); const text = result.output; const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); lines.pop(); // Remove last item that contains [v] and time in ms. @@ -105,7 +105,7 @@ for (const useIntermediateMergeReport of [false, true] as const) { await test.step('inner 2.2', async () => {}); }); });`, - }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PWTEST_TTY_WIDTH: '80' }); + }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' }); const text = result.output; const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); lines.pop(); // Remove last item that contains [v] and time in ms. @@ -135,7 +135,7 @@ for (const useIntermediateMergeReport of [false, true] as const) { console.log('a'.repeat(80) + 'b'.repeat(20)); }); `, - }, { reporter: 'list' }, { PWTEST_TTY_WIDTH: TTY_WIDTH + '' }); + }, { reporter: 'list' }, { PLAYWRIGHT_FORCE_TTY: TTY_WIDTH + '' }); const renderedText = simpleAnsiRenderer(result.rawOutput, TTY_WIDTH); if (process.platform === 'win32') @@ -154,7 +154,7 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(testInfo.retry).toBe(1); }); `, - }, { reporter: 'list', retries: '1' }, { PW_TEST_DEBUG_REPORTERS: '1', PWTEST_TTY_WIDTH: '80' }); + }, { reporter: 'list', retries: '1' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' }); const text = result.output; const lines = text.split('\n').filter(l => l.startsWith('0 :') || l.startsWith('1 :')).map(l => l.replace(/\d+(\.\d+)?m?s/, 'XXms')); @@ -185,10 +185,10 @@ for (const useIntermediateMergeReport of [false, true] as const) { test.skip('skipped very long name', async () => { }); `, - }, { reporter: 'list', retries: 0 }, { PWTEST_TTY_WIDTH: '50' }); + }, { reporter: 'list', retries: 0 }, { PLAYWRIGHT_FORCE_TTY: '50' }); expect(result.exitCode).toBe(1); - const lines = result.output.split('\n').slice(3, 11); + const lines = result.rawOutput.split('\n').map(line => line.split('\x1B[22m\x1B[1E')).flat().map(line => stripAnsi(line)).filter(line => line.trim()).slice(1, 9); expect(lines.every(line => line.length <= 50)).toBe(true); expect(lines[0]).toBe(` 1 …a.test.ts:3:15 › failure in very long name`); From 2734a0534256ffde6bd8dc8d27581c7dd26fe2a6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 15 May 2024 16:29:26 -0700 Subject: [PATCH 09/32] feat(trace-viewer): show nework request source id (#30810) image Fixes https://github.com/microsoft/playwright/issues/28903 --- packages/trace-viewer/src/ui/modelUtil.ts | 4 +- packages/trace-viewer/src/ui/networkTab.tsx | 92 +++++++++++++++++++-- tests/playwright-test/ui-mode-trace.spec.ts | 21 +++++ 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index 6385f577c8..ba866ad7ca 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -157,6 +157,8 @@ function indexModel(context: ContextEntry) { } for (const event of context.events) (event as any)[contextSymbol] = context; + for (const resource of context.resources) + (resource as any)[contextSymbol] = context; } function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) { @@ -330,7 +332,7 @@ export function idForAction(action: ActionTraceEvent) { return `${action.pageId || 'none'}:${action.callId}`; } -export function context(action: ActionTraceEvent | trace.EventTraceEvent): ContextEntry { +export function context(action: ActionTraceEvent | trace.EventTraceEvent | ResourceSnapshot): ContextEntry { return (action as any)[contextSymbol]; } diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx index b3caa501b5..d007d58198 100644 --- a/packages/trace-viewer/src/ui/networkTab.tsx +++ b/packages/trace-viewer/src/ui/networkTab.tsx @@ -21,12 +21,14 @@ import './networkTab.css'; import { NetworkResourceDetails } from './networkResourceDetails'; import { bytesToString, msToString } from '@web/uiUtils'; import { PlaceholderPanel } from './placeholderPanel'; -import type { MultiTraceModel } from './modelUtil'; +import { context, type MultiTraceModel } from './modelUtil'; import { GridView, type RenderedGridCell } from '@web/components/gridView'; import { SplitView } from '@web/components/splitView'; +import type { ContextEntry } from '../entries'; type NetworkTabModel = { resources: Entry[], + contextIdMap: ContextIdMap, }; type RenderedEntry = { @@ -39,6 +41,7 @@ type RenderedEntry = { start: number, route: string, resource: Entry, + contextId: string, }; type ColumnName = keyof RenderedEntry; type Sorting = { by: ColumnName, negate: boolean}; @@ -54,7 +57,8 @@ export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedT }); return filtered; }, [model, selectedTime]); - return { resources }; + const contextIdMap = React.useMemo(() => new ContextIdMap(model), [model]); + return { resources, contextIdMap }; } export const NetworkTab: React.FunctionComponent<{ @@ -66,11 +70,11 @@ export const NetworkTab: React.FunctionComponent<{ const [selectedEntry, setSelectedEntry] = React.useState(undefined); const { renderedEntries } = React.useMemo(() => { - const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries)); + const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries, networkModel.contextIdMap)); if (sorting) sort(renderedEntries, sorting); return { renderedEntries }; - }, [networkModel.resources, sorting, boundaries]); + }, [networkModel.resources, networkModel.contextIdMap, sorting, boundaries]); if (!networkModel.resources.length) return ; @@ -81,7 +85,7 @@ export const NetworkTab: React.FunctionComponent<{ selectedItem={selectedEntry} onSelected={item => setSelectedEntry(item)} onHighlighted={item => onEntryHovered(item?.resource)} - columns={selectedEntry ? ['name'] : ['name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route']} + columns={visibleColumns(!!selectedEntry, renderedEntries)} columnTitle={columnTitle} columnWidth={columnWidth} isError={item => item.status.code >= 400} @@ -100,6 +104,8 @@ export const NetworkTab: React.FunctionComponent<{ }; const columnTitle = (column: ColumnName) => { + if (column === 'contextId') + return 'Source'; if (column === 'name') return 'Name'; if (column === 'method') @@ -128,10 +134,28 @@ const columnWidth = (column: ColumnName) => { return 60; if (column === 'contentType') return 200; + if (column === 'contextId') + return 60; return 100; }; +function visibleColumns(entrySelected: boolean, renderedEntries: RenderedEntry[]): (keyof RenderedEntry)[] { + if (entrySelected) + return ['name']; + const columns: (keyof RenderedEntry)[] = []; + if (hasMultipleContexts(renderedEntries)) + columns.push('contextId'); + columns.push('name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route'); + return columns; +} + const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell => { + if (column === 'contextId') { + return { + body: entry.contextId, + title: entry.name.url, + }; + } if (column === 'name') { return { body: entry.name.name, @@ -159,7 +183,57 @@ const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell return { body: '' }; }; -const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry => { +class ContextIdMap { + private _pagerefToShortId = new Map(); + private _contextToId = new Map(); + private _lastPageId = 0; + private _lastApiRequestContextId = 0; + + constructor(model: MultiTraceModel | undefined) {} + + contextId(resource: Entry): string { + if (resource.pageref) + return this._pageId(resource.pageref); + else if (resource._apiRequest) + return this._apiRequestContextId(resource); + return ''; + } + + private _pageId(pageref: string): string { + let shortId = this._pagerefToShortId.get(pageref); + if (!shortId) { + ++this._lastPageId; + shortId = 'page#' + this._lastPageId; + this._pagerefToShortId.set(pageref, shortId); + } + return shortId; + } + + private _apiRequestContextId(resource: Entry): string { + const contextEntry = context(resource); + if (!contextEntry) + return ''; + let contextId = this._contextToId.get(contextEntry); + if (!contextId) { + ++this._lastApiRequestContextId; + contextId = 'api#' + this._lastApiRequestContextId; + this._contextToId.set(contextEntry, contextId); + } + return contextId; + } +} + +function hasMultipleContexts(renderedEntries: RenderedEntry[]): boolean { + const contextIds = new Set(); + for (const entry of renderedEntries) { + contextIds.add(entry.contextId); + if (contextIds.size > 1) + return true; + } + return false; +} + +const renderEntry = (resource: Entry, boundaries: Boundaries, contextIdGenerator: ContextIdMap): RenderedEntry => { const routeStatus = formatRouteStatus(resource); let resourceName: string; try { @@ -184,7 +258,8 @@ const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry => size: resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize, start: resource._monotonicTime! - boundaries.minimum, route: routeStatus, - resource + resource, + contextId: contextIdGenerator.contextId(resource), }; }; @@ -249,4 +324,7 @@ function comparator(sortBy: ColumnName) { return a.route.localeCompare(b.route); }; } + + if (sortBy === 'contextId') + return (a: RenderedEntry, b: RenderedEntry) => a.contextId.localeCompare(b.contextId); } diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 194e89afba..0f6194b25f 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -285,3 +285,24 @@ test('should reveal errors in the sourcetab', async ({ runUITest }) => { await page.getByText('a.spec.ts:4', { exact: true }).click(); await expect(page.locator('.source-line-running')).toContainText(`throw new Error('Oh my');`); }); + +test('should show request source context id', async ({ runUITest, server }) => { + const { page } = await runUITest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({ page, context, request }) => { + await page.goto('${server.EMPTY_PAGE}'); + const page2 = await context.newPage(); + await page2.goto('${server.EMPTY_PAGE}'); + await request.get('${server.EMPTY_PAGE}'); + }); + `, + }); + + await page.getByText('pass').dblclick(); + await page.getByText('Network', { exact: true }).click(); + await expect(page.locator('span').filter({ hasText: 'Source' })).toBeVisible(); + await expect(page.getByText('page#1')).toBeVisible(); + await expect(page.getByText('page#2')).toBeVisible(); + await expect(page.getByText('api#1')).toBeVisible(); +}); From 1526f1b52296c8648a1804b6ab9497af4b9abfa7 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 16 May 2024 20:10:27 +0200 Subject: [PATCH 10/32] chore: freeze webkit on macOS-12 (#30854) --- packages/playwright-core/browsers.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 45651d1a11..c7a02a7f5f 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -33,7 +33,9 @@ "mac10.14": "1446", "mac10.15": "1616", "mac11": "1816", - "mac11-arm64": "1816" + "mac11-arm64": "1816", + "mac12": "2009", + "mac12-arm64": "2009" }, "browserVersion": "17.4" }, From 4efb788f992619b8ebd30bcb7057c1fa3e78157d Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Thu, 16 May 2024 14:23:47 -0700 Subject: [PATCH 11/32] feat(chromium-tip-of-tree): roll to r1222 (#30859) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index c7a02a7f5f..7d0bc4b248 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1221", + "revision": "1222", "installByDefault": false, - "browserVersion": "126.0.6478.0" + "browserVersion": "126.0.6478.8" }, { "name": "firefox", From c9df73bc59799794ab137591828ad1e1e249d0ca Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 17 May 2024 00:45:16 -0700 Subject: [PATCH 12/32] feat(webkit): roll to r2010 (#30861) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 7d0bc4b248..fbfc10d3e0 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2009", + "revision": "2010", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 4ad94c1a8cb8430da089bbffbeb993a1d5378f03 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 17 May 2024 08:55:12 -0700 Subject: [PATCH 13/32] chore: print friendly localhost address from http server (#30853) --- .../src/server/trace/viewer/traceViewer.ts | 4 +-- .../playwright-core/src/utils/httpServer.ts | 29 +++++++++---------- packages/playwright/src/reporters/html.ts | 3 +- packages/playwright/src/runner/testServer.ts | 6 ++-- tests/playwright-test/reporter-blob.spec.ts | 4 +-- tests/playwright-test/reporter-html.spec.ts | 4 +-- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 3de6bec1b8..9dce56b387 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -145,7 +145,7 @@ export async function runTraceViewerApp(traceUrls: string[], browserName: string validateTraceUrls(traceUrls); const server = await startTraceViewerServer(options); await installRootRedirect(server, traceUrls, options); - const page = await openTraceViewerApp(server.urlPrefix(), browserName, options); + const page = await openTraceViewerApp(server.urlPrefix('precise'), browserName, options); if (exitOnClose) page.on('close', () => gracefullyProcessExitDoNotHang(0)); return page; @@ -155,7 +155,7 @@ export async function runTraceInBrowser(traceUrls: string[], options: TraceViewe validateTraceUrls(traceUrls); const server = await startTraceViewerServer(options); await installRootRedirect(server, traceUrls, options); - await openTraceInBrowser(server.urlPrefix()); + await openTraceInBrowser(server.urlPrefix('human-readable')); } export async function openTraceViewerApp(url: string, browserName: string, options?: TraceViewerAppOptions): Promise { diff --git a/packages/playwright-core/src/utils/httpServer.ts b/packages/playwright-core/src/utils/httpServer.ts index f387cefaa8..24a84ea502 100644 --- a/packages/playwright-core/src/utils/httpServer.ts +++ b/packages/playwright-core/src/utils/httpServer.ts @@ -34,14 +34,14 @@ export type Transport = { export class HttpServer { private _server: http.Server; - private _urlPrefix: string; + private _urlPrefixPrecise: string = ''; + private _urlPrefixHumanReadable: string = ''; private _port: number = 0; private _started = false; private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = []; private _wsGuid: string | undefined; - constructor(address: string = '') { - this._urlPrefix = address; + constructor() { this._server = createHttpServer(this._onRequest.bind(this)); } @@ -102,7 +102,7 @@ export class HttpServer { return this._wsGuid; } - async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise { + async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise { assert(!this._started, 'server already started'); this._started = true; @@ -121,24 +121,23 @@ export class HttpServer { const address = this._server.address(); assert(address, 'Could not bind server socket'); - if (!this._urlPrefix) { - if (typeof address === 'string') { - this._urlPrefix = address; - } else { - this._port = address.port; - const resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`; - this._urlPrefix = `http://${resolvedHost}:${address.port}`; - } + if (typeof address === 'string') { + this._urlPrefixPrecise = address; + this._urlPrefixHumanReadable = address; + } else { + this._port = address.port; + const resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`; + this._urlPrefixPrecise = `http://${resolvedHost}:${address.port}`; + this._urlPrefixHumanReadable = `http://${host}:${address.port}`; } - return this._urlPrefix; } async stop() { await new Promise(cb => this._server!.close(cb)); } - urlPrefix(): string { - return this._urlPrefix; + urlPrefix(purpose: 'human-readable' | 'precise'): string { + return purpose === 'human-readable' ? this._urlPrefixHumanReadable : this._urlPrefixPrecise; } serveFile(request: http.IncomingMessage, response: http.ServerResponse, absoluteFilePath: string, headers?: { [name: string]: string }): boolean { diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index c5562e0f79..2f152f447e 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -182,7 +182,8 @@ export async function showHTMLReport(reportFolder: string | undefined, host: str return; } const server = startHtmlReportServer(folder); - let url = await server.start({ port, host, preferredPort: port ? undefined : 9323 }); + await server.start({ port, host, preferredPort: port ? undefined : 9323 }); + let url = server.urlPrefix('human-readable'); console.log(''); console.log(colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`)); if (testId) diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 706e296da4..d2806b03f6 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -418,9 +418,9 @@ export async function runUIMode(configFile: string | undefined, options: TraceVi return await innerRunTestServer(configLocation, options, async (server: HttpServer, cancelPromise: ManualPromise) => { await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' }); if (options.host !== undefined || options.port !== undefined) { - await openTraceInBrowser(server.urlPrefix()); + await openTraceInBrowser(server.urlPrefix('human-readable')); } else { - const page = await openTraceViewerApp(server.urlPrefix(), 'chromium', { + const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', { headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1', persistentContextOptions: { handleSIGINT: false, @@ -435,7 +435,7 @@ export async function runTestServer(configFile: string | undefined, options: { h const configLocation = resolveConfigLocation(configFile); return await innerRunTestServer(configLocation, options, async server => { // eslint-disable-next-line no-console - console.log('Listening on ' + server.urlPrefix().replace('http:', 'ws:') + '/' + server.wsGuid()); + console.log('Listening on ' + server.urlPrefix('precise').replace('http:', 'ws:') + '/' + server.wsGuid()); }); } diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index dbd91049d1..5dfd3c7524 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -38,8 +38,8 @@ const test = baseTest.extend<{ await use(async (reportFolder?: string) => { reportFolder ??= test.info().outputPath('playwright-report'); server = startHtmlReportServer(reportFolder) as HttpServer; - const location = await server.start(); - await page.goto(location); + await server.start(); + await page.goto(server.urlPrefix('precise')); }); await server?.stop(); } diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 47f67a7bb9..e10302c4a1 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -29,8 +29,8 @@ const test = baseTest.extend<{ showReport: (reportFolder?: string) => Promise { reportFolder ??= testInfo.outputPath('playwright-report'); server = startHtmlReportServer(reportFolder) as HttpServer; - const location = await server.start(); - await page.goto(location); + await server.start(); + await page.goto(server.urlPrefix('precise')); }); await server?.stop(); } From b375f1077871f9025a1dc160932f8304e757725f Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 17 May 2024 09:32:40 -0700 Subject: [PATCH 14/32] fix: fulfill with unassigned status codes (#30856) Fixes https://github.com/microsoft/playwright/issues/30773 --- .../src/server/chromium/crNetworkManager.ts | 4 ++-- .../src/server/firefox/ffNetworkManager.ts | 2 +- packages/playwright-core/src/server/network.ts | 6 +++++- .../src/server/webkit/wkInterceptableRequest.ts | 2 +- tests/page/page-request-fulfill.spec.ts | 17 +++++++++-------- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index b78dd4084e..6411457a32 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -317,7 +317,7 @@ export class CRNetworkManager { requestPausedSessionInfo!.session._sendMayFail('Fetch.fulfillRequest', { requestId: requestPausedEvent.requestId, responseCode: 204, - responsePhrase: network.STATUS_TEXTS['204'], + responsePhrase: network.statusText(204), responseHeaders, body: '', }); @@ -622,7 +622,7 @@ class RouteImpl implements network.RouteDelegate { await this._session.send('Fetch.fulfillRequest', { requestId: this._interceptionId!, responseCode: response.status, - responsePhrase: network.STATUS_TEXTS[String(response.status)], + responsePhrase: network.statusText(response.status), responseHeaders, body, }); diff --git a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts index 081a1c9480..266f5bcb83 100644 --- a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts +++ b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts @@ -242,7 +242,7 @@ class FFRouteImpl implements network.RouteDelegate { await this._session.sendMayFail('Network.fulfillInterceptedRequest', { requestId: this._request._id, status: response.status, - statusText: network.STATUS_TEXTS[String(response.status)] || '', + statusText: network.statusText(response.status), headers: response.headers, base64body, }); diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index 83b0093a38..fd62e1751b 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -616,7 +616,7 @@ export interface RouteDelegate { } // List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes. -export const STATUS_TEXTS: { [status: string]: string } = { +const STATUS_TEXTS: { [status: string]: string } = { '100': 'Continue', '101': 'Switching Protocols', '102': 'Processing', @@ -682,6 +682,10 @@ export const STATUS_TEXTS: { [status: string]: string } = { '511': 'Network Authentication Required', }; +export function statusText(status: number): string { + return STATUS_TEXTS[String(status)] || 'Unknown'; +} + export function singleHeader(name: string, value: string): HeadersArray { return [{ name, value }]; } diff --git a/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts b/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts index 5218420480..f3c669dfc7 100644 --- a/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts +++ b/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts @@ -128,7 +128,7 @@ export class WKRouteImpl implements network.RouteDelegate { await this._session.sendMayFail('Network.interceptRequestWithResponse', { requestId: this._requestId, status: response.status, - statusText: network.STATUS_TEXTS[String(response.status)], + statusText: network.statusText(response.status), mimeType, headers, base64Encoded: response.isBase64, diff --git a/tests/page/page-request-fulfill.spec.ts b/tests/page/page-request-fulfill.spec.ts index 4a67b4353c..16f559b467 100644 --- a/tests/page/page-request-fulfill.spec.ts +++ b/tests/page/page-request-fulfill.spec.ts @@ -78,8 +78,9 @@ it('should work with status code 422', async ({ page, server }) => { expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!'); }); -it('should throw exception if status code is not supported', async ({ page, server, browserName }) => { +it('should fulfill with unuassigned status codes', async ({ page, server, browserName }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28490' }); + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30773' }); let fulfillPromiseCallback; const fulfillPromise = new Promise(f => fulfillPromiseCallback = f); await page.route('**/data.json', route => { @@ -89,14 +90,14 @@ it('should throw exception if status code is not supported', async ({ page, serv }).catch(e => e)); }); await page.goto(server.EMPTY_PAGE); - page.evaluate(url => fetch(url), server.PREFIX + '/data.json').catch(() => {}); + const response = await page.evaluate(async url => { + const { status, statusText } = await fetch(url); + return { status, statusText }; + }, server.PREFIX + '/data.json'); const error = await fulfillPromise; - if (browserName === 'chromium') { - expect(error).toBeTruthy(); - expect(error.message).toContain(' Invalid http status code or phrase'); - } else { - expect(error).toBe(undefined); - } + expect(error).toBe(undefined); + expect(response.status).toBe(430); + expect(response.statusText).toBe('Unknown'); }); it('should not throw if request was cancelled by the page', async ({ page, server }) => { From dcaded525566cdd6a6a58ca124b5c01fe404ed0c Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 17 May 2024 09:32:57 -0700 Subject: [PATCH 15/32] chore(trace-viewer): format negative duration as - (#30840) --- packages/web/src/uiUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts index 7e00693234..95d0d42a53 100644 --- a/packages/web/src/uiUtils.ts +++ b/packages/web/src/uiUtils.ts @@ -55,7 +55,7 @@ export function useMeasure() { } export function msToString(ms: number): string { - if (!isFinite(ms)) + if (ms < 0 || !isFinite(ms)) return '-'; if (ms === 0) From b6a7d0a17e97bb5b937e92ba4fc62baa387e0a13 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 17 May 2024 10:06:11 -0700 Subject: [PATCH 16/32] feat(chromium): roll to r1119 (#30879) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 4 +- packages/playwright-core/browsers.json | 4 +- .../src/server/chromium/protocol.d.ts | 137 ++++++++++++++++-- .../src/server/deviceDescriptorsSource.json | 96 ++++++------ packages/playwright-core/types/protocol.d.ts | 137 ++++++++++++++++-- 5 files changed, 308 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 668ee8097b..393fd2113c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-125.0.6422.41-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-125.0.1-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-126.0.6478.8-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-125.0.1-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 125.0.6422.41 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 126.0.6478.8 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 17.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 125.0.1 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index fbfc10d3e0..9d7e59138b 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,9 +3,9 @@ "browsers": [ { "name": "chromium", - "revision": "1118", + "revision": "1119", "installByDefault": true, - "browserVersion": "125.0.6422.41" + "browserVersion": "126.0.6478.8" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index 6d8a2559db..ad0ed7fc4c 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -841,6 +841,7 @@ CORS RFC1918 enforcement. clientSecurityState?: Network.ClientSecurityState; } export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader"; + export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType"; /** * Details for issues around "Attribution Reporting API" usage. Explainer: https://github.com/WICG/attribution-reporting-api @@ -870,6 +871,10 @@ instead of "limited-quirks". url: string; location?: SourceCodeLocation; } + export interface SharedDictionaryIssueDetails { + sharedDictionaryError: SharedDictionaryError; + request: AffectedRequest; + } export type GenericIssueErrorType = "CrossOriginPortalPostMessageError"|"FormLabelForNameError"|"FormDuplicateIdForInputError"|"FormInputWithNoLabelError"|"FormAutocompleteAttributeEmptyError"|"FormEmptyIdAndNameAttributesForInputError"|"FormAriaLabelledByToNonExistingId"|"FormInputAssignedAutocompleteValueToIdOrNameAttributeError"|"FormLabelHasNeitherForNorNestedInput"|"FormLabelForMatchesNonExistingIdError"|"FormInputHasWrongButWellIntendedAutocompleteValueError"|"ResponseWasBlockedByORB"; /** * Depending on the concrete errorType, different properties are set. @@ -997,7 +1002,7 @@ registrations being ignored. optional fields in InspectorIssueDetails to convey more specific information about the kind of issue. */ - export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"; + export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"; /** * This struct holds a list of optional fields with additional information specific to the kind of issue. When adding a new issue code, please also @@ -1024,6 +1029,7 @@ add a new optional field to this type. stylesheetLoadingIssueDetails?: StylesheetLoadingIssueDetails; propertyRuleIssueDetails?: PropertyRuleIssueDetails; federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails; + sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails; } /** * A unique id for a DevTools inspector issue. Allows other entities (e.g. @@ -1121,6 +1127,33 @@ using Audits.issueAdded event. } } + /** + * Defines commands and events for browser extensions. Available if the client +is connected using the --remote-debugging-pipe flag and +the --enable-unsafe-extension-debugging flag is set. + */ + export module Extensions { + + + /** + * Installs an unpacked extension from the filesystem similar to +--load-extension CLI flags. Returns extension ID once the extension +has been installed. + */ + export type loadUnpackedParameters = { + /** + * Absolute file path. + */ + path: string; + } + export type loadUnpackedReturnValue = { + /** + * Extension id. + */ + id: string; + } + } + /** * Defines commands and events for Autofill. */ @@ -3450,7 +3483,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-markers"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; + export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-markers"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; /** * Shadow root type. */ @@ -4426,6 +4459,25 @@ appear on top of all other content. */ nodeIds: NodeId[]; } + /** + * Returns the NodeId of the matched element according to certain relations. + */ + export type getElementByRelationParameters = { + /** + * Id of the node from which to query the relation. + */ + nodeId: NodeId; + /** + * Type of relation to get. + */ + relation: "PopoverTarget"; + } + export type getElementByRelationReturnValue = { + /** + * NodeId of the element matching the queried relation. + */ + nodeId: NodeId; + } /** * Re-does the last undone action. */ @@ -8309,8 +8361,16 @@ records. */ export type ServiceWorkerRouterSource = "network"|"cache"|"fetch-event"|"race-network-and-fetch-handler"; export interface ServiceWorkerRouterInfo { - ruleIdMatched: number; - matchedSourceType: ServiceWorkerRouterSource; + /** + * ID of the rule matched. If there is a matched rule, this field will +be set, otherwiser no value will be set. + */ + ruleIdMatched?: number; + /** + * The router source of the matched rule. If there is a matched rule, this +field will be set, otherwise no value will be set. + */ + matchedSourceType?: ServiceWorkerRouterSource; } /** * HTTP response data. @@ -8385,7 +8445,10 @@ records. */ fromEarlyHints?: boolean; /** - * Information about how Service Worker Static Router was used. + * Information about how ServiceWorker Static Router API was used. If this +field is set with `matchedSourceType` field, a matching rule is found. +If this field is set without `matchedSource`, no matching rule is found. +Otherwise, the API is not used. */ serviceWorkerRouterInfo?: ServiceWorkerRouterInfo; /** @@ -11167,7 +11230,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"window-placement"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -11755,7 +11818,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl /** * List of not restored reasons for back-forward cache. */ - export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"; + export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"; /** * Types of not restored reasons for back-forward cache. */ @@ -12334,7 +12397,7 @@ option, use with caution. This API always waits for the manifest to be loaded. If manifestId is provided, and it does not match the manifest of the current document, this API errors out. - If there isn’t a loaded page, this API errors out immediately. + If there is not a loaded page, this API errors out immediately. */ export type getAppManifestParameters = { manifestId?: string; @@ -16690,7 +16753,7 @@ possible for multiple rule sets and links to trigger a single attempt. /** * List of FinalStatus reasons for Prerender2. */ - export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"; + export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"; /** * Preloading status values, see also PreloadingTriggeringOutcome. This status is shared by prefetchStatusUpdated and prerenderStatusUpdated. @@ -16921,6 +16984,52 @@ https://web.dev/learn/pwa/web-app-manifest. badgeCount: number; fileHandlers: FileHandler[]; } + /** + * Installs the given manifest identity, optionally using the given install_url +or IWA bundle location. + +TODO(crbug.com/337872319) Support IWA to meet the following specific +requirement. +IWA-specific install description: If the manifest_id is isolated-app://, +install_url_or_bundle_url is required, and can be either an http(s) URL or +file:// URL pointing to a signed web bundle (.swbn). The .swbn file's +signing key must correspond to manifest_id. If Chrome is not in IWA dev +mode, the installation will fail, regardless of the state of the allowlist. + */ + export type installParameters = { + manifestId: string; + /** + * The location of the app or bundle overriding the one derived from the +manifestId. + */ + installUrlOrBundleUrl?: string; + } + export type installReturnValue = { + } + /** + * Uninstals the given manifest_id and closes any opened app windows. + */ + export type uninstallParameters = { + manifestId: string; + } + export type uninstallReturnValue = { + } + /** + * Launches the installed web app, or an url in the same web app instead of the +default start url if it is provided. Returns a tab / web contents based +Target.TargetID which can be used to attach to via Target.attachToTarget or +similar APIs. + */ + export type launchParameters = { + manifestId: string; + url?: string; + } + export type launchReturnValue = { + /** + * ID of the tab target created as a result. + */ + targetId: Target.TargetID; + } } /** @@ -19773,6 +19882,7 @@ Error was thrown. "Audits.enable": Audits.enableParameters; "Audits.checkContrast": Audits.checkContrastParameters; "Audits.checkFormsIssues": Audits.checkFormsIssuesParameters; + "Extensions.loadUnpacked": Extensions.loadUnpackedParameters; "Autofill.trigger": Autofill.triggerParameters; "Autofill.setAddresses": Autofill.setAddressesParameters; "Autofill.disable": Autofill.disableParameters; @@ -19870,6 +19980,7 @@ Error was thrown. "DOM.querySelector": DOM.querySelectorParameters; "DOM.querySelectorAll": DOM.querySelectorAllParameters; "DOM.getTopLayerElements": DOM.getTopLayerElementsParameters; + "DOM.getElementByRelation": DOM.getElementByRelationParameters; "DOM.redo": DOM.redoParameters; "DOM.removeAttribute": DOM.removeAttributeParameters; "DOM.removeNode": DOM.removeNodeParameters; @@ -20256,6 +20367,9 @@ Error was thrown. "FedCm.dismissDialog": FedCm.dismissDialogParameters; "FedCm.resetCooldown": FedCm.resetCooldownParameters; "PWA.getOsAppState": PWA.getOsAppStateParameters; + "PWA.install": PWA.installParameters; + "PWA.uninstall": PWA.uninstallParameters; + "PWA.launch": PWA.launchParameters; "Console.clearMessages": Console.clearMessagesParameters; "Console.disable": Console.disableParameters; "Console.enable": Console.enableParameters; @@ -20361,6 +20475,7 @@ Error was thrown. "Audits.enable": Audits.enableReturnValue; "Audits.checkContrast": Audits.checkContrastReturnValue; "Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue; + "Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue; "Autofill.trigger": Autofill.triggerReturnValue; "Autofill.setAddresses": Autofill.setAddressesReturnValue; "Autofill.disable": Autofill.disableReturnValue; @@ -20458,6 +20573,7 @@ Error was thrown. "DOM.querySelector": DOM.querySelectorReturnValue; "DOM.querySelectorAll": DOM.querySelectorAllReturnValue; "DOM.getTopLayerElements": DOM.getTopLayerElementsReturnValue; + "DOM.getElementByRelation": DOM.getElementByRelationReturnValue; "DOM.redo": DOM.redoReturnValue; "DOM.removeAttribute": DOM.removeAttributeReturnValue; "DOM.removeNode": DOM.removeNodeReturnValue; @@ -20844,6 +20960,9 @@ Error was thrown. "FedCm.dismissDialog": FedCm.dismissDialogReturnValue; "FedCm.resetCooldown": FedCm.resetCooldownReturnValue; "PWA.getOsAppState": PWA.getOsAppStateReturnValue; + "PWA.install": PWA.installReturnValue; + "PWA.uninstall": PWA.uninstallReturnValue; + "PWA.launch": PWA.launchReturnValue; "Console.clearMessages": Console.clearMessagesReturnValue; "Console.disable": Console.disableReturnValue; "Console.enable": Console.enableReturnValue; diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 777b816d63..40b3823952 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -978,7 +978,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -989,7 +989,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1000,7 +1000,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1011,7 +1011,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1022,7 +1022,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1033,7 +1033,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1044,7 +1044,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1055,7 +1055,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1066,7 +1066,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1077,7 +1077,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1088,7 +1088,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1099,7 +1099,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1110,7 +1110,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1121,7 +1121,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1132,7 +1132,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1143,7 +1143,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1154,7 +1154,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1165,7 +1165,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1176,7 +1176,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1187,7 +1187,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1242,7 +1242,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1253,7 +1253,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1264,7 +1264,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1275,7 +1275,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1286,7 +1286,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1297,7 +1297,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1308,7 +1308,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1319,7 +1319,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1330,7 +1330,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1345,7 +1345,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1360,7 +1360,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1375,7 +1375,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1390,7 +1390,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1405,7 +1405,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1420,7 +1420,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1431,7 +1431,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1442,7 +1442,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1457,7 +1457,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36 Edg/125.0.6422.41", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36 Edg/126.0.6478.8", "screen": { "width": 1792, "height": 1120 @@ -1502,7 +1502,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1517,7 +1517,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.41 Safari/537.36 Edg/125.0.6422.41", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36 Edg/126.0.6478.8", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts index 6d8a2559db..ad0ed7fc4c 100644 --- a/packages/playwright-core/types/protocol.d.ts +++ b/packages/playwright-core/types/protocol.d.ts @@ -841,6 +841,7 @@ CORS RFC1918 enforcement. clientSecurityState?: Network.ClientSecurityState; } export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader"; + export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType"; /** * Details for issues around "Attribution Reporting API" usage. Explainer: https://github.com/WICG/attribution-reporting-api @@ -870,6 +871,10 @@ instead of "limited-quirks". url: string; location?: SourceCodeLocation; } + export interface SharedDictionaryIssueDetails { + sharedDictionaryError: SharedDictionaryError; + request: AffectedRequest; + } export type GenericIssueErrorType = "CrossOriginPortalPostMessageError"|"FormLabelForNameError"|"FormDuplicateIdForInputError"|"FormInputWithNoLabelError"|"FormAutocompleteAttributeEmptyError"|"FormEmptyIdAndNameAttributesForInputError"|"FormAriaLabelledByToNonExistingId"|"FormInputAssignedAutocompleteValueToIdOrNameAttributeError"|"FormLabelHasNeitherForNorNestedInput"|"FormLabelForMatchesNonExistingIdError"|"FormInputHasWrongButWellIntendedAutocompleteValueError"|"ResponseWasBlockedByORB"; /** * Depending on the concrete errorType, different properties are set. @@ -997,7 +1002,7 @@ registrations being ignored. optional fields in InspectorIssueDetails to convey more specific information about the kind of issue. */ - export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"; + export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"; /** * This struct holds a list of optional fields with additional information specific to the kind of issue. When adding a new issue code, please also @@ -1024,6 +1029,7 @@ add a new optional field to this type. stylesheetLoadingIssueDetails?: StylesheetLoadingIssueDetails; propertyRuleIssueDetails?: PropertyRuleIssueDetails; federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails; + sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails; } /** * A unique id for a DevTools inspector issue. Allows other entities (e.g. @@ -1121,6 +1127,33 @@ using Audits.issueAdded event. } } + /** + * Defines commands and events for browser extensions. Available if the client +is connected using the --remote-debugging-pipe flag and +the --enable-unsafe-extension-debugging flag is set. + */ + export module Extensions { + + + /** + * Installs an unpacked extension from the filesystem similar to +--load-extension CLI flags. Returns extension ID once the extension +has been installed. + */ + export type loadUnpackedParameters = { + /** + * Absolute file path. + */ + path: string; + } + export type loadUnpackedReturnValue = { + /** + * Extension id. + */ + id: string; + } + } + /** * Defines commands and events for Autofill. */ @@ -3450,7 +3483,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-markers"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; + export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-markers"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; /** * Shadow root type. */ @@ -4426,6 +4459,25 @@ appear on top of all other content. */ nodeIds: NodeId[]; } + /** + * Returns the NodeId of the matched element according to certain relations. + */ + export type getElementByRelationParameters = { + /** + * Id of the node from which to query the relation. + */ + nodeId: NodeId; + /** + * Type of relation to get. + */ + relation: "PopoverTarget"; + } + export type getElementByRelationReturnValue = { + /** + * NodeId of the element matching the queried relation. + */ + nodeId: NodeId; + } /** * Re-does the last undone action. */ @@ -8309,8 +8361,16 @@ records. */ export type ServiceWorkerRouterSource = "network"|"cache"|"fetch-event"|"race-network-and-fetch-handler"; export interface ServiceWorkerRouterInfo { - ruleIdMatched: number; - matchedSourceType: ServiceWorkerRouterSource; + /** + * ID of the rule matched. If there is a matched rule, this field will +be set, otherwiser no value will be set. + */ + ruleIdMatched?: number; + /** + * The router source of the matched rule. If there is a matched rule, this +field will be set, otherwise no value will be set. + */ + matchedSourceType?: ServiceWorkerRouterSource; } /** * HTTP response data. @@ -8385,7 +8445,10 @@ records. */ fromEarlyHints?: boolean; /** - * Information about how Service Worker Static Router was used. + * Information about how ServiceWorker Static Router API was used. If this +field is set with `matchedSourceType` field, a matching rule is found. +If this field is set without `matchedSource`, no matching rule is found. +Otherwise, the API is not used. */ serviceWorkerRouterInfo?: ServiceWorkerRouterInfo; /** @@ -11167,7 +11230,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"window-placement"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -11755,7 +11818,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl /** * List of not restored reasons for back-forward cache. */ - export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"; + export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"; /** * Types of not restored reasons for back-forward cache. */ @@ -12334,7 +12397,7 @@ option, use with caution. This API always waits for the manifest to be loaded. If manifestId is provided, and it does not match the manifest of the current document, this API errors out. - If there isn’t a loaded page, this API errors out immediately. + If there is not a loaded page, this API errors out immediately. */ export type getAppManifestParameters = { manifestId?: string; @@ -16690,7 +16753,7 @@ possible for multiple rule sets and links to trigger a single attempt. /** * List of FinalStatus reasons for Prerender2. */ - export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"; + export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"; /** * Preloading status values, see also PreloadingTriggeringOutcome. This status is shared by prefetchStatusUpdated and prerenderStatusUpdated. @@ -16921,6 +16984,52 @@ https://web.dev/learn/pwa/web-app-manifest. badgeCount: number; fileHandlers: FileHandler[]; } + /** + * Installs the given manifest identity, optionally using the given install_url +or IWA bundle location. + +TODO(crbug.com/337872319) Support IWA to meet the following specific +requirement. +IWA-specific install description: If the manifest_id is isolated-app://, +install_url_or_bundle_url is required, and can be either an http(s) URL or +file:// URL pointing to a signed web bundle (.swbn). The .swbn file's +signing key must correspond to manifest_id. If Chrome is not in IWA dev +mode, the installation will fail, regardless of the state of the allowlist. + */ + export type installParameters = { + manifestId: string; + /** + * The location of the app or bundle overriding the one derived from the +manifestId. + */ + installUrlOrBundleUrl?: string; + } + export type installReturnValue = { + } + /** + * Uninstals the given manifest_id and closes any opened app windows. + */ + export type uninstallParameters = { + manifestId: string; + } + export type uninstallReturnValue = { + } + /** + * Launches the installed web app, or an url in the same web app instead of the +default start url if it is provided. Returns a tab / web contents based +Target.TargetID which can be used to attach to via Target.attachToTarget or +similar APIs. + */ + export type launchParameters = { + manifestId: string; + url?: string; + } + export type launchReturnValue = { + /** + * ID of the tab target created as a result. + */ + targetId: Target.TargetID; + } } /** @@ -19773,6 +19882,7 @@ Error was thrown. "Audits.enable": Audits.enableParameters; "Audits.checkContrast": Audits.checkContrastParameters; "Audits.checkFormsIssues": Audits.checkFormsIssuesParameters; + "Extensions.loadUnpacked": Extensions.loadUnpackedParameters; "Autofill.trigger": Autofill.triggerParameters; "Autofill.setAddresses": Autofill.setAddressesParameters; "Autofill.disable": Autofill.disableParameters; @@ -19870,6 +19980,7 @@ Error was thrown. "DOM.querySelector": DOM.querySelectorParameters; "DOM.querySelectorAll": DOM.querySelectorAllParameters; "DOM.getTopLayerElements": DOM.getTopLayerElementsParameters; + "DOM.getElementByRelation": DOM.getElementByRelationParameters; "DOM.redo": DOM.redoParameters; "DOM.removeAttribute": DOM.removeAttributeParameters; "DOM.removeNode": DOM.removeNodeParameters; @@ -20256,6 +20367,9 @@ Error was thrown. "FedCm.dismissDialog": FedCm.dismissDialogParameters; "FedCm.resetCooldown": FedCm.resetCooldownParameters; "PWA.getOsAppState": PWA.getOsAppStateParameters; + "PWA.install": PWA.installParameters; + "PWA.uninstall": PWA.uninstallParameters; + "PWA.launch": PWA.launchParameters; "Console.clearMessages": Console.clearMessagesParameters; "Console.disable": Console.disableParameters; "Console.enable": Console.enableParameters; @@ -20361,6 +20475,7 @@ Error was thrown. "Audits.enable": Audits.enableReturnValue; "Audits.checkContrast": Audits.checkContrastReturnValue; "Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue; + "Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue; "Autofill.trigger": Autofill.triggerReturnValue; "Autofill.setAddresses": Autofill.setAddressesReturnValue; "Autofill.disable": Autofill.disableReturnValue; @@ -20458,6 +20573,7 @@ Error was thrown. "DOM.querySelector": DOM.querySelectorReturnValue; "DOM.querySelectorAll": DOM.querySelectorAllReturnValue; "DOM.getTopLayerElements": DOM.getTopLayerElementsReturnValue; + "DOM.getElementByRelation": DOM.getElementByRelationReturnValue; "DOM.redo": DOM.redoReturnValue; "DOM.removeAttribute": DOM.removeAttributeReturnValue; "DOM.removeNode": DOM.removeNodeReturnValue; @@ -20844,6 +20960,9 @@ Error was thrown. "FedCm.dismissDialog": FedCm.dismissDialogReturnValue; "FedCm.resetCooldown": FedCm.resetCooldownReturnValue; "PWA.getOsAppState": PWA.getOsAppStateReturnValue; + "PWA.install": PWA.installReturnValue; + "PWA.uninstall": PWA.uninstallReturnValue; + "PWA.launch": PWA.launchReturnValue; "Console.clearMessages": Console.clearMessagesReturnValue; "Console.disable": Console.disableReturnValue; "Console.enable": Console.enableReturnValue; From 9188ff7917a6b79f47e36fc547b703486e381a80 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 17 May 2024 23:18:16 +0200 Subject: [PATCH 17/32] docs: add release-notes for language ports (#30868) --- docs/src/release-notes-csharp.md | 76 ++++++++++++++++++++++++++++++++ docs/src/release-notes-java.md | 66 +++++++++++++++++++++++++++ docs/src/release-notes-python.md | 54 +++++++++++++++++++++++ 3 files changed, 196 insertions(+) diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index df22402b87..54d91386cb 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -4,6 +4,82 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.44 + +### New APIs + +**Accessibility assertions** + +- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name: + ```csharp + var locator = Page.GetByRole(AriaRole.Button); + await Expect(locator).ToHaveAccessibleNameAsync("Submit"); + ``` + +- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description: + ```csharp + var locator = Page.GetByRole(AriaRole.Button); + await Expect(locator).ToHaveAccessibleDescriptionAsync("Upload a photo"); + ``` + +- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role: + ```csharp + var locator = Page.GetByTestId("save-button"); + await Expect(locator).ToHaveRoleAsync(AriaRole.Button); + ``` + +**Locator handler** + +- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `NoWaitAfter` option. +- You can use new `Times` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run. +- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument. +- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers. + +```csharp +var locator = Page.GetByText("This interstitial covers the button"); +await Page.AddLocatorHandlerAsync(locator, async (overlay) => +{ + await overlay.Locator("#close").ClickAsync(); +}, new() { Times = 3, NoWaitAfter = true }); +// Run your tests that can be interrupted by the overlay. +// ... +await Page.RemoveLocatorHandlerAsync(locator); +``` + +**Miscellaneous options** + +- New method [`method: FormData.append`] allows to specify repeating fields with the same name in [`Multipart`](./api/class-apirequestcontext#api-request-context-fetch-option-multipart) option in `APIRequestContext.FetchAsync()`: +- ``` + ```csharp + var formData = Context.APIRequest.CreateFormData(); + formData.Append("file", new FilePayload() + { + Name = "f1.js", + MimeType = "text/javascript", + Buffer = System.Text.Encoding.UTF8.GetBytes("var x = 2024;") + }); + formData.Append("file", new FilePayload() + { + Name = "f2.txt", + MimeType = "text/plain", + Buffer = System.Text.Encoding.UTF8.GetBytes("hello") + }); + var response = await Context.APIRequest.PostAsync("https://example.com/uploadFiles", new() { Multipart = formData }); + ``` + +- [`method: PageAssertions.toHaveURL`] now supports `IgnoreCase` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case). + +### Browser Versions + +* Chromium 125.0.6422.14 +* Mozilla Firefox 125.0.1 +* WebKit 17.4 + +This version was also tested against the following stable channels: + +* Google Chrome 124 +* Microsoft Edge 124 + ## Version 1.43 ### New APIs diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index 579d135f58..4a46556f68 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -4,6 +4,72 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.44 + +### New APIs + +**Accessibility assertions** + +- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name: + ```java + Locator locator = page.getByRole(AriaRole.BUTTON); + assertThat(locator).hasAccessibleName("Submit"); + ``` + +- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description: + ```java + Locator locator = page.getByRole(AriaRole.BUTTON); + assertThat(locator).hasAccessibleDescription("Upload a photo"); + ``` + +- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role: + ```java + Locator locator = page.getByTestId("save-button"); + assertThat(locator).hasRole(AriaRole.BUTTON); + ``` + +**Locator handler** + +- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `setNoWaitAfter` option. +- You can use new `setTimes` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run. +- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument. +- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers. + +```java +Locator locator = page.getByText("This interstitial covers the button"); +page.addLocatorHandler(locator, overlay -> { + overlay.locator("#close").click(); +}, new Page.AddLocatorHandlerOptions().setTimes(3).setNoWaitAfter(true)); +// Run your tests that can be interrupted by the overlay. +// ... +page.removeLocatorHandler(locator); +``` + +**Miscellaneous options** + +- New method [`method: FormData.append`] allows to specify repeating fields with the same name in [`setMultipart`](./api/class-requestoptions#request-options-set-multipart) option in `RequestOptions`: + ```java + FormData formData = FormData.create(); + formData.append("file", new FilePayload("f1.js", "text/javascript", + "var x = 2024;".getBytes(StandardCharsets.UTF_8))); + formData.append("file", new FilePayload("f2.txt", "text/plain", + "hello".getBytes(StandardCharsets.UTF_8))); + APIResponse response = context.request().post("https://example.com/uploadFile", RequestOptions.create().setMultipart(formData)); + ``` + +- `expect(page).toHaveURL(url)` now supports `setIgnoreCase` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case). + +### Browser Versions + +* Chromium 125.0.6422.14 +* Mozilla Firefox 125.0.1 +* WebKit 17.4 + +This version was also tested against the following stable channels: + +* Google Chrome 124 +* Microsoft Edge 124 + ## Version 1.43 ### New APIs diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index 789a89150c..590a4edb88 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -4,6 +4,60 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.44 + +### New APIs + +**Accessibility assertions** + +- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name: + ```python + locator = page.get_by_role("button") + expect(locator).to_have_accessible_name("Submit") + ``` + +- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description: + ```python + locator = page.get_by_role("button") + expect(locator).to_have_accessible_description("Upload a photo") + ``` + +- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role: + ```python + locator = page.get_by_test_id("save-button") + expect(locator).to_have_role("button") + ``` + +**Locator handler** + +- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `no_wait_after` option. +- You can use new `times` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run. +- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument. +- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers. + +```python +locator = page.get_by_text("This interstitial covers the button") +page.add_locator_handler(locator, lambda overlay: overlay.locator("#close").click(), times=3, no_wait_after=True) +# Run your tests that can be interrupted by the overlay. +# ... +page.remove_locator_handler(locator) +``` + +**Miscellaneous options** + +- [`method: PageAssertions.toHaveURL`] now supports `ignore_case` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case). + +### Browser Versions + +* Chromium 125.0.6422.14 +* Mozilla Firefox 125.0.1 +* WebKit 17.4 + +This version was also tested against the following stable channels: + +* Google Chrome 124 +* Microsoft Edge 124 + ## Version 1.43 ### New APIs From 162c18f586afbe9472077cba5f6aff2ecd506aa6 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Sun, 19 May 2024 16:28:32 +0200 Subject: [PATCH 18/32] feat(roll): roll Firefox to r1450 (#30865) Fixes https://github.com/microsoft/playwright/actions/runs/9120975643/job/25079367394 --- README.md | 4 +- packages/playwright-core/browsers.json | 8 ++-- .../src/server/deviceDescriptorsSource.json | 4 +- .../src/server/registry/index.ts | 44 +++++++++---------- tests/page/page-network-request.spec.ts | 23 +++++----- utils/roll_browser.js | 2 - 6 files changed, 40 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 393fd2113c..3206b72800 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-126.0.6478.8-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-125.0.1-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-126.0.6478.8-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-126.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | :--- | :---: | :---: | :---: | | Chromium 126.0.6478.8 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 17.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Firefox 125.0.1 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 126.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details. diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 9d7e59138b..1def3af185 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,15 +15,15 @@ }, { "name": "firefox", - "revision": "1449", + "revision": "1450", "installByDefault": true, - "browserVersion": "125.0.1" + "browserVersion": "126.0" }, { "name": "firefox-beta", - "revision": "1449", + "revision": "1450", "installByDefault": false, - "browserVersion": "126.0b1" + "browserVersion": "127.0b3" }, { "name": "webkit", diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 40b3823952..1c75d5ad9d 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -1472,7 +1472,7 @@ "defaultBrowserType": "chromium" }, "Desktop Firefox HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0.1) Gecko/20100101 Firefox/125.0.1", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0", "screen": { "width": 1792, "height": 1120 @@ -1532,7 +1532,7 @@ "defaultBrowserType": "chromium" }, "Desktop Firefox": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0.1) Gecko/20100101 Firefox/125.0.1", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 9cec89ebd3..b23865663c 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -137,17 +137,17 @@ const DOWNLOAD_PATHS: Record = { 'debian11-arm64': 'builds/firefox/%s/firefox-debian-11-arm64.zip', 'debian12-x64': 'builds/firefox/%s/firefox-debian-12.zip', 'debian12-arm64': 'builds/firefox/%s/firefox-debian-12-arm64.zip', - 'mac10.13': 'builds/firefox/%s/firefox-mac-13.zip', - 'mac10.14': 'builds/firefox/%s/firefox-mac-13.zip', - 'mac10.15': 'builds/firefox/%s/firefox-mac-13.zip', - 'mac11': 'builds/firefox/%s/firefox-mac-13.zip', - 'mac11-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip', - 'mac12': 'builds/firefox/%s/firefox-mac-13.zip', - 'mac12-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip', - 'mac13': 'builds/firefox/%s/firefox-mac-13.zip', - 'mac13-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip', - 'mac14': 'builds/firefox/%s/firefox-mac-13.zip', - 'mac14-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip', + 'mac10.13': 'builds/firefox/%s/firefox-mac.zip', + 'mac10.14': 'builds/firefox/%s/firefox-mac.zip', + 'mac10.15': 'builds/firefox/%s/firefox-mac.zip', + 'mac11': 'builds/firefox/%s/firefox-mac.zip', + 'mac11-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip', + 'mac12': 'builds/firefox/%s/firefox-mac.zip', + 'mac12-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip', + 'mac13': 'builds/firefox/%s/firefox-mac.zip', + 'mac13-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip', + 'mac14': 'builds/firefox/%s/firefox-mac.zip', + 'mac14-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip', 'win64': 'builds/firefox/%s/firefox-win64.zip', }, 'firefox-beta': { @@ -162,17 +162,17 @@ const DOWNLOAD_PATHS: Record = { 'debian11-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-11-arm64.zip', 'debian12-x64': 'builds/firefox-beta/%s/firefox-beta-debian-12.zip', 'debian12-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-12-arm64.zip', - 'mac10.13': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip', - 'mac10.14': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip', - 'mac10.15': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip', - 'mac11': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip', - 'mac11-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip', - 'mac12': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip', - 'mac12-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip', - 'mac13': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip', - 'mac13-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip', - 'mac14': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip', - 'mac14-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip', + 'mac10.13': 'builds/firefox-beta/%s/firefox-beta-mac.zip', + 'mac10.14': 'builds/firefox-beta/%s/firefox-beta-mac.zip', + 'mac10.15': 'builds/firefox-beta/%s/firefox-beta-mac.zip', + 'mac11': 'builds/firefox-beta/%s/firefox-beta-mac.zip', + 'mac11-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip', + 'mac12': 'builds/firefox-beta/%s/firefox-beta-mac.zip', + 'mac12-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip', + 'mac13': 'builds/firefox-beta/%s/firefox-beta-mac.zip', + 'mac13-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip', + 'mac14': 'builds/firefox-beta/%s/firefox-beta-mac.zip', + 'mac14-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip', 'win64': 'builds/firefox-beta/%s/firefox-beta-win64.zip', }, 'webkit': { diff --git a/tests/page/page-network-request.spec.ts b/tests/page/page-network-request.spec.ts index 903fd4cc1d..472bb34560 100644 --- a/tests/page/page-network-request.spec.ts +++ b/tests/page/page-network-request.spec.ts @@ -19,11 +19,9 @@ import { test as it, expect } from './pageTest'; import { attachFrame } from '../config/utils'; import fs from 'fs'; -function adjustServerHeaders(headers: Object, browserName: string, channel: string) { - if (browserName === 'firefox' && channel === 'firefox-beta') { - // This is a new experimental feature, only enabled in Firefox Beta for now. +function adjustServerHeaders(headers: Object, browserName: string) { + if (browserName === 'firefox') delete headers['priority']; - } return headers; } @@ -90,7 +88,7 @@ it('should return headers', async ({ page, server, browserName }) => { expect(response.request().headers()['user-agent']).toContain('WebKit'); }); -it('should get the same headers as the server', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => { +it('should get the same headers as the server', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => { it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99'); it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language'); let serverRequest; @@ -100,10 +98,10 @@ it('should get the same headers as the server', async ({ page, server, browserNa }); const response = await page.goto(server.PREFIX + '/empty.html'); const headers = await response.request().allHeaders(); - expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName, channel)); + expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName)); }); -it('should not return allHeaders() until they are available', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => { +it('should not return allHeaders() until they are available', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => { it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99'); it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language'); @@ -122,13 +120,13 @@ it('should not return allHeaders() until they are available', async ({ page, ser await page.goto(server.PREFIX + '/empty.html'); const requestHeaders = await requestHeadersPromise; - expect(requestHeaders).toEqual(adjustServerHeaders(serverRequest.headers, browserName, channel)); + expect(requestHeaders).toEqual(adjustServerHeaders(serverRequest.headers, browserName)); const responseHeaders = await responseHeadersPromise; expect(responseHeaders['foo']).toBe('bar'); }); -it('should get the same headers as the server CORS', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => { +it('should get the same headers as the server CORS', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, }) => { it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99'); it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language'); @@ -147,7 +145,7 @@ it('should get the same headers as the server CORS', async ({ page, server, brow expect(text).toBe('done'); const response = await responsePromise; const headers = await response.request().allHeaders(); - expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName, channel)); + expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName)); }); it('should not get preflight CORS requests when intercepting', async ({ page, server, browserName, isAndroid }) => { @@ -408,10 +406,9 @@ it('should report raw headers', async ({ page, server, browserName, platform, is return { name, value: values[0] }; }); } - if (browserName === 'firefox' && channel === 'firefox-beta') { - // This is a new experimental feature, only enabled in Firefox Beta for now. + if (browserName === 'firefox') expectedHeaders = expectedHeaders.filter(({ name }) => name.toLowerCase() !== 'priority'); - } + res.end(); }); await page.goto(server.EMPTY_PAGE); diff --git a/utils/roll_browser.js b/utils/roll_browser.js index ba5c41b379..a57b4f5004 100755 --- a/utils/roll_browser.js +++ b/utils/roll_browser.js @@ -61,8 +61,6 @@ Example: 'wk': 'webkit', }[args[0].toLowerCase()] ?? args[0].toLowerCase(); const descriptors = [browsersJSON.browsers.find(b => b.name === browserName)]; - if (browserName === 'firefox') - descriptors.push(browsersJSON.browsers.find(b => b.name === 'firefox-asan')); if (!descriptors.every(d => !!d)) { console.log(`Unknown browser "${browserName}"`); From 7fd3539ebd139484af4994876a7f6ca47e108ee4 Mon Sep 17 00:00:00 2001 From: Atmaram Naik <21354507+atmnk@users.noreply.github.com> Date: Mon, 20 May 2024 19:44:35 +0530 Subject: [PATCH 19/32] docs(intro): adds all three package manager commands (#30884) --- docs/src/intro-js.md | 160 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/docs/src/intro-js.md b/docs/src/intro-js.md index d6ac4a3e10..c742ce6fa8 100644 --- a/docs/src/intro-js.md +++ b/docs/src/intro-js.md @@ -50,6 +50,7 @@ pnpm create playwright ``` + @@ -81,10 +82,40 @@ The `tests` folder contains a basic example test to help you get started with te By default tests will be run on all 3 browsers, chromium, firefox and webkit using 3 workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal. + + + ```bash npx playwright test ``` + + + + +```bash +yarn playwright test +``` + + + + + +```bash +pnpm exec playwright test +``` + + + + + ![tests running in command line](https://github.com/microsoft/playwright/assets/13063165/981c1b2b-dc7e-4b85-b241-272b44da6628) See our doc on [Running Tests](./running-tests.md) to learn more about running tests in headed mode, running multiple tests, running specific tests etc. @@ -92,19 +123,81 @@ See our doc on [Running Tests](./running-tests.md) to learn more about running t After your test completes, an [HTML Reporter](./test-reporters.md#html-reporter) will be generated, which shows you a full report of your tests allowing you to filter the report by browsers, passed tests, failed tests, skipped tests and flaky tests. You can click on each test and explore the test's errors as well as each step of the test. By default, the HTML report is opened automatically if some of the tests failed. + + + ```bash npx playwright show-report ``` + + + + +```bash +yarn playwright show-report +``` + + + + + +```bash +pnpm exec playwright show-report +``` + + + + + ![HTML Report](https://github.com/microsoft/playwright/assets/13063165/38ec17a7-9e61-4002-b137-a93812765501) ## Running the Example Test in UI Mode Run your tests with [UI Mode](./test-ui-mode.md) for a better developer experience with time travel debugging, watch mode and more. + + + + ```bash npx playwright test --ui ``` + + + + + +```bash +yarn playwright test --ui +``` + + + + + +```bash +pnpm exec playwright test --ui +``` + + + + + ![UI Mode](https://github.com/microsoft/playwright/assets/13063165/c5b501cc-4f5d-485a-87cc-66044c651786) Check out or [detailed guide on UI Mode](./test-ui-mode.md) to learn more about its features. @@ -113,17 +206,84 @@ Check out or [detailed guide on UI Mode](./test-ui-mode.md) to learn more about To update Playwright to the latest version run the following command: + + + + ```bash npm install -D @playwright/test@latest # Also download new browser binaries and their dependencies: npx playwright install --with-deps ``` + + + + + +```bash +yarn add --dev @playwright/test@latest +# Also download new browser binaries and their dependencies: +yarn playwright install --with-deps +``` + + + + + +```bash +pnpm install --save-dev @playwright/test@latest +# Also download new browser binaries and their dependencies: +pnpm exec playwright install --with-deps +``` + + + + + You can always check which version of Playwright you have by running the following command: + + + + ```bash npx playwright --version ``` + + + + +```bash +yarn playwright --version +``` + + + + + +```bash +pnpm exec playwright --version +``` + + + + + ## System requirements - Node.js 18+ From 437b14a90359234fa89ecef5a13991955920ebc0 Mon Sep 17 00:00:00 2001 From: Lukas Bockstaller Date: Mon, 20 May 2024 18:18:08 +0100 Subject: [PATCH 20/32] fix: relative url path for ui mode (#29924) This is a follow up #29564 I did a deep dive on a redirect issue I observed in my infrastructure and originally attributed to some configuration mistakes on my part. I have code hosted on `example.com/code` and use subdomain proxying. This leads to the uimode being exposed on `example.com/code/proxy/{{port}}`. Clicking on the open uimode link shown by vscode redirected with a 302 to `example.com/proxy/{{port}}` The absolute redirect url overruled the relative path handling reverse proxies rely on. This PR turns the absolute into a relative url to avoid this issue. --- packages/playwright-core/src/server/trace/viewer/traceViewer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 9dce56b387..741a927757 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -132,7 +132,7 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ for (const reporter of options.reporter || []) params.append('reporter', reporter); - const urlPath = `/trace/${options.webApp || 'index.html'}?${params.toString()}`; + const urlPath = `./trace/${options.webApp || 'index.html'}?${params.toString()}`; server.routePath('/', (_, response) => { response.statusCode = 302; response.setHeader('Location', urlPath); From b67b9634c1b813921fc12d54e52050fa9dcb0b65 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 20 May 2024 10:30:32 -0700 Subject: [PATCH 21/32] chore: remove support for "experimental" from documentation (#30880) Also add support for "hidden" and make `generate_types/index` actually pass tsc checks. --- utils/doclint/api_parser.js | 56 ++++++++++++++++++++------------- utils/doclint/documentation.js | 57 ++-------------------------------- utils/generate_types/index.js | 46 ++++++++++++++------------- 3 files changed, 62 insertions(+), 97 deletions(-) diff --git a/utils/doclint/api_parser.js b/utils/doclint/api_parser.js index 223d562542..105c56c658 100644 --- a/utils/doclint/api_parser.js +++ b/utils/doclint/api_parser.js @@ -77,7 +77,10 @@ class ApiParser { continue; } } - const clazz = new docs.Class(extractMetainfo(node), name, [], extendsName, extractComments(node)); + const metainfo = extractMetainfo(node); + const clazz = new docs.Class(metainfo, name, [], extendsName, extractComments(node)); + if (metainfo.hidden) + return; this.classes.set(clazz.name, clazz); } @@ -103,13 +106,14 @@ class ApiParser { returnType = new docs.Type('void'); const comments = extractComments(spec); + const metainfo = extractMetainfo(spec); let member; if (match[1] === 'event') - member = docs.Member.createEvent(extractMetainfo(spec), name, returnType, comments); + member = docs.Member.createEvent(metainfo, name, returnType, comments); if (match[1] === 'property') - member = docs.Member.createProperty(extractMetainfo(spec), name, returnType, comments, !optional); + member = docs.Member.createProperty(metainfo, name, returnType, comments, !optional); if (['method', 'async method', 'optional method', 'optional async method'].includes(match[1])) { - member = docs.Member.createMethod(extractMetainfo(spec), name, [], returnType, comments); + member = docs.Member.createMethod(metainfo, name, [], returnType, comments); if (match[1].includes('async')) member.async = true; if (match[1].includes('optional')) @@ -119,6 +123,11 @@ class ApiParser { throw new Error('Unknown member: ' + spec.text); const clazz = /** @type {docs.Class} */(this.classes.get(match[2])); + if (!clazz) + throw new Error(`Unknown class ${match[2]} for member: ` + spec.text); + if (metainfo.hidden) + return; + const existingMember = clazz.membersArray.find(m => m.name === name && m.kind === member.kind); if (existingMember && isTypeOverride(existingMember, member)) { for (const lang of member?.langs?.only || []) { @@ -157,6 +166,8 @@ class ApiParser { throw new Error('Invalid member name ' + spec.text); if (match[1] === 'param') { const arg = this.parseProperty(spec); + if (!arg) + return; arg.name = name; const existingArg = method.argsArray.find(m => m.name === arg.name); if (existingArg && isTypeOverride(existingArg, arg)) { @@ -171,13 +182,15 @@ class ApiParser { } } else { // match[1] === 'option' + const p = this.parseProperty(spec); + if (!p) + return; let options = method.argsArray.find(o => o.name === 'options'); if (!options) { const type = new docs.Type('Object', []); - options = docs.Member.createProperty({ langs: {}, experimental: false, since: 'v1.0', deprecated: undefined, discouraged: undefined }, 'options', type, undefined, false); + options = docs.Member.createProperty({ langs: {}, since: 'v1.0', deprecated: undefined, discouraged: undefined }, 'options', type, undefined, false); method.argsArray.push(options); } - const p = this.parseProperty(spec); p.required = false; // @ts-ignore options.type.properties.push(p); @@ -186,6 +199,7 @@ class ApiParser { /** * @param {MarkdownHeaderNode} spec + * @returns {docs.Member | null} */ parseProperty(spec) { const param = childrenWithoutProperties(spec)[0]; @@ -196,12 +210,15 @@ class ApiParser { const name = text.substring(0, typeStart).replace(/\`/g, '').trim(); const comments = extractComments(spec); const { type, optional } = this.parseType(/** @type {MarkdownLiNode} */(param)); - return docs.Member.createProperty(extractMetainfo(spec), name, type, comments, !optional); + const metainfo = extractMetainfo(spec); + if (metainfo.hidden) + return null; + return docs.Member.createProperty(metainfo, name, type, comments, !optional); } /** * @param {MarkdownLiNode} spec - * @return {{ type: docs.Type, optional: boolean, experimental: boolean }} + * @return {{ type: docs.Type, optional: boolean }} */ parseType(spec) { const arg = parseVariable(spec.text); @@ -210,16 +227,16 @@ class ApiParser { const { name, text } = parseVariable(/** @type {string} */(child.text)); const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]); const childType = this.parseType(child); - properties.push(docs.Member.createProperty({ langs: {}, experimental: childType.experimental, since: 'v1.0', deprecated: undefined, discouraged: undefined }, name, childType.type, comments, !childType.optional)); + properties.push(docs.Member.createProperty({ langs: {}, since: 'v1.0', deprecated: undefined, discouraged: undefined }, name, childType.type, comments, !childType.optional)); } const type = docs.Type.parse(arg.type, properties); - return { type, optional: arg.optional, experimental: arg.experimental }; + return { type, optional: arg.optional }; } } /** * @param {string} line - * @returns {{ name: string, type: string, text: string, optional: boolean, experimental: boolean }} + * @returns {{ name: string, type: string, text: string, optional: boolean }} */ function parseVariable(line) { let match = line.match(/^`([^`]+)` (.*)/); @@ -234,12 +251,9 @@ function parseVariable(line) { const name = match[1]; let remainder = match[2]; let optional = false; - let experimental = false; - while ('?e'.includes(remainder[0])) { + while ('?'.includes(remainder[0])) { if (remainder[0] === '?') optional = true; - else if (remainder[0] === 'e') - experimental = true; remainder = remainder.substring(1); } if (!remainder.startsWith('<')) @@ -252,7 +266,7 @@ function parseVariable(line) { if (c === '>') --depth; if (depth === 0) - return { name, type: remainder.substring(1, i), text: remainder.substring(i + 2), optional, experimental }; + return { name, type: remainder.substring(1, i), text: remainder.substring(i + 2), optional }; } throw new Error('Should not be reached, line: ' + line); } @@ -344,15 +358,15 @@ function parseApi(apiDir, paramsPath) { /** * @param {MarkdownHeaderNode} spec - * @returns {import('./documentation').Metainfo} + * @returns {import('./documentation').Metainfo & { hidden: boolean }} */ function extractMetainfo(spec) { return { langs: extractLangs(spec), since: extractSince(spec), - experimental: extractExperimental(spec), deprecated: extractAttribute(spec, 'deprecated'), discouraged: extractAttribute(spec, 'discouraged'), + hidden: extractHidden(spec), }; } @@ -402,9 +416,9 @@ function extractSince(spec) { * @param {MarkdownHeaderNode} spec * @returns {boolean} */ - function extractExperimental(spec) { + function extractHidden(spec) { for (const child of spec.children) { - if (child.type === 'li' && child.liType === 'bullet' && child.text === 'experimental') + if (child.type === 'li' && child.liType === 'bullet' && child.text === 'hidden') return true; } return false; @@ -429,7 +443,7 @@ function extractSince(spec) { */ function childrenWithoutProperties(spec) { return (spec.children || []).filter(c => { - const isProperty = c.type === 'li' && c.liType === 'bullet' && (c.text.startsWith('langs:') || c.text.startsWith('since:') || c.text.startsWith('deprecated:') || c.text.startsWith('discouraged:') || c.text === 'experimental'); + const isProperty = c.type === 'li' && c.liType === 'bullet' && (c.text.startsWith('langs:') || c.text.startsWith('since:') || c.text.startsWith('deprecated:') || c.text.startsWith('discouraged:') || c.text === 'hidden'); return !isProperty; }); } diff --git a/utils/doclint/documentation.js b/utils/doclint/documentation.js index f0527c4120..b93998bd33 100644 --- a/utils/doclint/documentation.js +++ b/utils/doclint/documentation.js @@ -57,7 +57,6 @@ const md = require('../markdown'); * since: string, * deprecated?: string | undefined, * discouraged?: string | undefined, - * experimental: boolean * }} Metainfo */ @@ -132,18 +131,6 @@ class Documentation { this.index(); } - filterOutExperimental() { - const classesArray = []; - for (const clazz of this.classesArray) { - if (clazz.experimental) - continue; - clazz.filterOutExperimental(); - classesArray.push(clazz); - } - this.classesArray = classesArray; - this.index(); - } - index() { for (const cls of this.classesArray) { this.classes.set(cls.name, cls); @@ -231,7 +218,6 @@ class Documentation { */ constructor(metainfo, name, membersArray, extendsName = null, spec = undefined) { this.langs = metainfo.langs; - this.experimental = metainfo.experimental; this.since = metainfo.since; this.deprecated = metainfo.deprecated; this.discouraged = metainfo.discouraged; @@ -286,7 +272,7 @@ class Documentation { } clone() { - const cls = new Class({ langs: this.langs, experimental: this.experimental, since: this.since, deprecated: this.deprecated, discouraged: this.discouraged }, this.name, this.membersArray.map(m => m.clone()), this.extends, this.spec); + const cls = new Class({ langs: this.langs, since: this.since, deprecated: this.deprecated, discouraged: this.discouraged }, this.name, this.membersArray.map(m => m.clone()), this.extends, this.spec); cls.comment = this.comment; return cls; } @@ -306,17 +292,6 @@ class Documentation { this.membersArray = membersArray; } - filterOutExperimental() { - const membersArray = []; - for (const member of this.membersArray) { - if (member.experimental) - continue; - member.filterOutExperimental(); - membersArray.push(member); - } - this.membersArray = membersArray; - } - sortMembers() { /** * @param {Member} member @@ -362,7 +337,6 @@ class Member { constructor(kind, metainfo, name, type, argsArray, spec = undefined, required = true) { this.kind = kind; this.langs = metainfo.langs; - this.experimental = metainfo.experimental; this.since = metainfo.since; this.deprecated = metainfo.deprecated; this.discouraged = metainfo.discouraged; @@ -447,22 +421,8 @@ class Member { } } - filterOutExperimental() { - if (!this.type) - return; - this.type.filterOutExperimental(); - const argsArray = []; - for (const arg of this.argsArray) { - if (arg.experimental || !arg.type) - continue; - arg.type.filterOutExperimental(); - argsArray.push(arg); - } - this.argsArray = argsArray; - } - clone() { - const result = new Member(this.kind, { langs: this.langs, experimental: this.experimental, since: this.since, deprecated: this.deprecated, discouraged: this.discouraged }, this.name, this.type?.clone(), this.argsArray.map(arg => arg.clone()), this.spec, this.required); + const result = new Member(this.kind, { langs: this.langs, since: this.since, deprecated: this.deprecated, discouraged: this.discouraged }, this.name, this.type?.clone(), this.argsArray.map(arg => arg.clone()), this.spec, this.required); result.alias = this.alias; result.async = this.async; result.paramOrOption = this.paramOrOption; @@ -671,19 +631,6 @@ class Type { this.properties = properties; } - filterOutExperimental() { - if (!this.properties) - return; - const properties = []; - for (const prop of this.properties) { - if (prop.experimental) - continue; - prop.filterOutExperimental(); - properties.push(prop); - } - this.properties = properties; - } - /** * @param {Type[]} result */ diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index d9dde814b7..209ce04756 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -36,7 +36,6 @@ class TypesGenerator { * ignoreMissing?: Set, * doNotExportClassNames?: Set, * doNotGenerate?: Set, - * includeExperimental?: boolean, * }} options */ constructor(options) { @@ -50,8 +49,6 @@ class TypesGenerator { this.doNotExportClassNames = options.doNotExportClassNames || new Set(); this.doNotGenerate = options.doNotGenerate || new Set(); this.documentation.filterForLanguage('js'); - if (!options.includeExperimental) - this.documentation.filterOutExperimental(); this.documentation.copyDocsFromSuperclasses([]); this.injectDisposeAsync(); } @@ -65,7 +62,7 @@ class TypesGenerator { continue; if (!member.async) continue; - newMember = new docs.Member('method', { langs: {}, since: '1.0', experimental: false }, '[Symbol.asyncDispose]', null, []); + newMember = new docs.Member('method', { langs: {}, since: '1.0' }, '[Symbol.asyncDispose]', null, []); newMember.async = true; break; } @@ -98,12 +95,20 @@ class TypesGenerator { }, (className, methodName, overloadIndex) => { if (className === 'SuiteFunction' && methodName === '__call') { const cls = this.documentation.classes.get('Test'); + if (!cls) + throw new Error(`Unknown class "Test"`); const method = cls.membersArray.find(m => m.alias === 'describe'); + if (!method) + throw new Error(`Unknown method "Test.describe"`); return this.memberJSDOC(method, ' ').trimLeft(); } if (className === 'TestFunction' && methodName === '__call') { const cls = this.documentation.classes.get('Test'); + if (!cls) + throw new Error(`Unknown class "Test"`); const method = cls.membersArray.find(m => m.alias === '(call)'); + if (!method) + throw new Error(`Unknown method "Test.(call)"`); return this.memberJSDOC(method, ' ').trimLeft(); } @@ -137,6 +142,8 @@ class TypesGenerator { .filter(cls => !handledClasses.has(cls.name)); { const playwright = this.documentation.classesArray.find(c => c.name === 'Playwright'); + if (!playwright) + throw new Error(`Unknown class "Playwright"`); playwright.membersArray = playwright.membersArray.filter(member => !['errors', 'devices'].includes(member.name)); playwright.index(); } @@ -327,19 +334,20 @@ class TypesGenerator { hasOwnMethod(classDesc, member) { if (this.handledMethods.has(`${classDesc.name}.${member.alias}#${member.overloadIndex}`)) return false; - while (classDesc = this.parentClass(classDesc)) { - if (classDesc.members.has(member.alias)) + let parent = /** @type {docs.Class | undefined} */ (classDesc); + while (parent = this.parentClass(parent)) { + if (parent.members.has(member.alias)) return false; } return true; } /** - * @param {docs.Class} classDesc + * @param {docs.Class | undefined} classDesc */ parentClass(classDesc) { - if (!classDesc.extends) - return null; + if (!classDesc || !classDesc.extends) + return; return this.documentation.classes.get(classDesc.extends); } @@ -427,6 +435,8 @@ class TypesGenerator { const name = namespace.map(n => n[0].toUpperCase() + n.substring(1)).join(''); const shouldExport = exported[name]; const properties = namespace[namespace.length - 1] === 'options' ? type.sortedProperties() : type.properties; + if (!properties) + throw new Error(`Object type must have properties`); if (!this.objectDefinitions.some(o => o.name === name)) this.objectDefinitions.push({ name, properties }); if (shouldExport) { @@ -503,15 +513,13 @@ class TypesGenerator { ]); /** - * @param {boolean} includeExperimental * @returns {Promise} */ - async function generateCoreTypes(includeExperimental) { + async function generateCoreTypes() { const documentation = coreDocumentation.clone(); const generator = new TypesGenerator({ documentation, doNotGenerate: assertionClasses, - includeExperimental, }); let types = await generator.generateTypes(path.join(__dirname, 'overrides.d.ts')); const namedDevices = Object.keys(devices).map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`).join('\n'); @@ -534,10 +542,9 @@ class TypesGenerator { } /** - * @param {boolean} includeExperimental * @returns {Promise} */ - async function generateTestTypes(includeExperimental) { + async function generateTestTypes() { const documentation = coreDocumentation.mergeWith(testDocumentation); const generator = new TypesGenerator({ documentation, @@ -574,16 +581,14 @@ class TypesGenerator { 'TestFunction', ]), doNotExportClassNames: assertionClasses, - includeExperimental, }); return await generator.generateTypes(path.join(__dirname, 'overrides-test.d.ts')); } /** - * @param {boolean} includeExperimental * @returns {Promise} */ - async function generateReporterTypes(includeExperimental) { + async function generateReporterTypes() { const documentation = coreDocumentation.mergeWith(testDocumentation).mergeWith(reporterDocumentation); const generator = new TypesGenerator({ documentation, @@ -601,7 +606,6 @@ class TypesGenerator { 'JSONReportTestResult', 'JSONReportTestStep', ]), - includeExperimental, }); return await generator.generateTypes(path.join(__dirname, 'overrides-testReporter.d.ts')); } @@ -629,9 +633,9 @@ class TypesGenerator { if (!fs.existsSync(playwrightTypesDir)) fs.mkdirSync(playwrightTypesDir) writeFile(path.join(coreTypesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'packages', 'playwright-core', 'src', 'server', 'chromium', 'protocol.d.ts'), 'utf8'), false); - writeFile(path.join(coreTypesDir, 'types.d.ts'), await generateCoreTypes(false), true); - writeFile(path.join(playwrightTypesDir, 'test.d.ts'), await generateTestTypes(false), true); - writeFile(path.join(playwrightTypesDir, 'testReporter.d.ts'), await generateReporterTypes(false), true); + writeFile(path.join(coreTypesDir, 'types.d.ts'), await generateCoreTypes(), true); + writeFile(path.join(playwrightTypesDir, 'test.d.ts'), await generateTestTypes(), true); + writeFile(path.join(playwrightTypesDir, 'testReporter.d.ts'), await generateReporterTypes(), true); process.exit(0); })().catch(e => { console.error(e); From 042896472b8ae233437d9e3e937cdae9fdce954b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 20 May 2024 16:36:57 -0700 Subject: [PATCH 22/32] fix: route.continue should not change multipart form data body (#30863) The bug was fixed in https://github.com/microsoft/playwright/pull/30734. This PR adds a test and updates interception logic to not send post data when it is not modified. Fixes https://github.com/microsoft/playwright/issues/30788 --- .../playwright-core/src/client/network.ts | 5 +-- tests/page/page-request-continue.spec.ts | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 4e22a27dd6..36ade499cf 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -101,7 +101,6 @@ export class Request extends ChannelOwner implements ap if (this._redirectedFrom) this._redirectedFrom._redirectedTo = this; this._provisionalHeaders = new RawHeaders(initializer.headers); - this._fallbackOverrides.postDataBuffer = initializer.postData; this._timing = { startTime: 0, domainLookupStart: -1, @@ -128,11 +127,11 @@ export class Request extends ChannelOwner implements ap } postData(): string | null { - return this._fallbackOverrides.postDataBuffer?.toString('utf-8') || null; + return (this._fallbackOverrides.postDataBuffer || this._initializer.postData)?.toString('utf-8') || null; } postDataBuffer(): Buffer | null { - return this._fallbackOverrides.postDataBuffer || null; + return this._fallbackOverrides.postDataBuffer || this._initializer.postData || null; } postDataJSON(): Object | null { diff --git a/tests/page/page-request-continue.spec.ts b/tests/page/page-request-continue.spec.ts index 1b7f78e40d..9230bb5fa8 100644 --- a/tests/page/page-request-continue.spec.ts +++ b/tests/page/page-request-continue.spec.ts @@ -477,3 +477,46 @@ it('should intercept css variable with background url', async ({ page, server }) await page.waitForTimeout(1000); expect(interceptedRequests).toBe(1); }); + +it('continue should not change multipart/form-data body', async ({ page, server, browserName }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/19158' }); + await page.goto(server.EMPTY_PAGE); + server.setRoute('/upload', (request, response) => { + response.writeHead(200, { 'Content-Type': 'text/plain' }); + response.end('done'); + }); + async function sendFormData() { + const reqPromise = server.waitForRequest('/upload'); + const status = await page.evaluate(async () => { + const newFile = new File(['file content'], 'file.txt'); + const formData = new FormData(); + formData.append('file', newFile); + const response = await fetch('/upload', { + method: 'POST', + credentials: 'include', + body: formData, + }); + return response.status; + }); + const req = await reqPromise; + expect(status).toBe(200); + return req; + } + const reqBefore = await sendFormData(); + await page.route('**/*', async route => { + await route.continue(); + }); + const reqAfter = await sendFormData(); + const fileContent = [ + 'Content-Disposition: form-data; name=\"file\"; filename=\"file.txt\"', + 'Content-Type: application/octet-stream', + '', + 'file content', + '------'].join('\r\n'); + expect.soft((await reqBefore.postBody).toString('utf8')).toContain(fileContent); + expect.soft((await reqAfter.postBody).toString('utf8')).toContain(fileContent); + // Firefox sends a bit longer boundary. + const expectedLength = browserName === 'firefox' ? '246' : '208'; + expect.soft(reqBefore.headers['content-length']).toBe(expectedLength); + expect.soft(reqAfter.headers['content-length']).toBe(expectedLength); +}); From a93ad3dadea86e3e1d555c5bb9c2a19458db656b Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 21 May 2024 09:15:33 +0200 Subject: [PATCH 23/32] fix(fetch): allow UTF-8 in Location header (#30904) --- packages/playwright-core/src/server/fetch.ts | 9 ++++-- tests/library/browsercontext-fetch.spec.ts | 29 ++++++++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 56125f2e5d..e15c5aca8a 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -336,12 +336,15 @@ export abstract class APIRequestContext extends SdkObject { redirectOptions.rejectUnauthorized = false; // HTTP-redirect fetch step 4: If locationURL is null, then return response. - if (response.headers.location) { + // Best-effort UTF-8 decoding, per spec it's US-ASCII only, but browsers are more lenient. + // Node.js parses it as Latin1 via std::v8::String, so we convert it to UTF-8. + const locationHeaderValue = Buffer.from(response.headers.location ?? '', 'latin1').toString('utf8'); + if (locationHeaderValue) { let locationURL; try { - locationURL = new URL(response.headers.location, url); + locationURL = new URL(locationHeaderValue, url); } catch (error) { - reject(new Error(`uri requested responds with an invalid redirect URL: ${response.headers.location}`)); + reject(new Error(`uri requested responds with an invalid redirect URL: ${locationHeaderValue}`)); request.destroy(); return; } diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 0efb4bacb6..e4d7a6dbf3 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -198,6 +198,20 @@ it('should follow redirects', async ({ context, server }) => { expect(await response.json()).toEqual({ foo: 'bar' }); }); +it('should follow redirects correctly when Location header contains UTF-8 characters', async ({ context, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30903' }); + server.setRoute('/redirect', (req, res) => { + // Node.js only allows US-ASCII, so we can't send invalid headers directly. Sending it as a raw response instead. + res.socket.write('HTTP/1.1 301 Moved Permanently\r\n'); + res.socket.write(`Location: ${server.PREFIX}/empty.html?message=マスクПривет\r\n`); + res.socket.write('\r\n'); + res.socket.uncork(); + res.socket.end(); + }); + const response = await context.request.get(server.PREFIX + '/redirect'); + expect(response.url()).toBe(server.PREFIX + '/empty.html?' + new URLSearchParams({ message: 'マスクПривет' })); +}); + it('should add cookies from Set-Cookie header', async ({ context, page, server }) => { server.setRoute('/setcookie.html', (req, res) => { res.setHeader('Set-Cookie', ['session=value', 'foo=bar; max-age=3600']); @@ -794,21 +808,6 @@ it('should respect timeout after redirects', async function({ context, server }) expect(error.message).toContain(`Request timed out after 100ms`); }); -it('should throw on a redirect with an invalid URL', async ({ context, server }) => { - server.setRedirect('/redirect', '/test'); - server.setRoute('/test', (req, res) => { - // Node.js prevents us from responding with an invalid header, therefore we manually write the response. - const conn = res.connection!; - conn.write('HTTP/1.1 302\r\n'); - conn.write('Location: https://здравствуйте/\r\n'); - conn.write('\r\n'); - conn.uncork(); - conn.end(); - }); - const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e); - expect(error.message).toContain('apiRequestContext.get: uri requested responds with an invalid redirect URL'); -}); - it('should not hang on a brotli encoded Range request', async ({ context, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/18190' }); it.skip(+process.versions.node.split('.')[0] < 18); From 6290af3a0831d20de87a58120288f2f5b27c5f2c Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 21 May 2024 10:46:52 -0700 Subject: [PATCH 24/32] feat(reporters): align and document environment variables (#30912) - Documents `PLAYWRIGHT_FORCE_TTY` and `FORCE_COLOR` across terminal reporters. - New `PLAYWRIGHT_LIST_PRINT_STEPS`. Removes undocumented test-only `PW_TEST_DEBUG_REPORTERS_PRINT_STEPS`. - Replaces `PLAYWRIGHT_HTML_REPORT` with `PLAYWRIGHT_HTML_OUTPUT_DIR` and `PW_TEST_HTML_REPORT_OPEN` with `PLAYWRIGHT_HTML_OPEN` for consistency, supports older versions for backwards compatibility. - New `PLAYWRIGHT_HTML_HOST`, `PLAYWRIGHT_HTML_PORT` and `PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL`. - New `PLAYWRIGHT_JUNIT_STRIP_ANSI` and `PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME`. - Removes `PW_HTML_REPORT` that was set for unknown reason. --- docs/src/test-reporters-js.md | 44 ++++++- packages/playwright-core/src/utils/env.ts | 8 +- packages/playwright/src/reporters/html.ts | 32 ++--- packages/playwright/src/reporters/junit.ts | 5 +- packages/playwright/src/reporters/list.ts | 7 +- .../playwright-test-fixtures.ts | 1 + tests/playwright-test/reporter-blob.spec.ts | 32 ++--- tests/playwright-test/reporter-html.spec.ts | 119 +++++++++--------- tests/playwright-test/reporter-json.spec.ts | 4 +- tests/playwright-test/reporter-list.spec.ts | 2 +- 10 files changed, 151 insertions(+), 103 deletions(-) diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md index ef46ad4fb0..7b0f91fea4 100644 --- a/docs/src/test-reporters-js.md +++ b/docs/src/test-reporters-js.md @@ -97,6 +97,15 @@ export default defineConfig({ }); ``` +List report supports the following configuration options and environment variables: + +| Environment Variable Name | Reporter Config Option| Description | Default +|---|---|---|---| +| `PLAYWRIGHT_LIST_PRINT_STEPS` | `printSteps` | Whether to print each step on its own line. | `false` +| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. | `true` when terminal is in TTY mode, `false` otherwise. +| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise. + + ### Line reporter Line reporter is more concise than the list reporter. It uses a single line to report last finished test, and prints failures when they occur. Line reporter is useful for large test suites where it shows the progress but does not spam the output by listing all the tests. @@ -127,6 +136,14 @@ Running 124 tests using 6 workers [23/124] gitignore.spec.ts - should respect nested .gitignore ``` +Line report supports the following configuration options and environment variables: + +| Environment Variable Name | Reporter Config Option| Description | Default +|---|---|---|---| +| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. | `true` when terminal is in TTY mode, `false` otherwise. +| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise. + + ### Dot reporter Dot reporter is very concise - it only produces a single character per successful test run. It is the default on CI and useful where you don't want a lot of output. @@ -150,6 +167,15 @@ Running 124 tests using 6 workers ······F············································· ``` + +Dot report supports the following configuration options and environment variables: + +| Environment Variable Name | Reporter Config Option| Description | Default +|---|---|---|---| +| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. | `true` when terminal is in TTY mode, `false` otherwise. +| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise. + + ### HTML reporter HTML reporter produces a self-contained folder that contains report for the test run that can be served as a web page. @@ -159,7 +185,7 @@ npx playwright test --reporter=html ``` By default, HTML report is opened automatically if some of the tests failed. You can control this behavior via the -`open` property in the Playwright config or the `PW_TEST_HTML_REPORT_OPEN` environmental variable. The possible values for that property are `always`, `never` and `on-failure` +`open` property in the Playwright config or the `PLAYWRIGHT_HTML_OPEN` environmental variable. The possible values for that property are `always`, `never` and `on-failure` (default). You can also configure `host` and `port` that are used to serve the HTML report. @@ -173,7 +199,7 @@ export default defineConfig({ ``` By default, report is written into the `playwright-report` folder in the current working directory. One can override -that location using the `PLAYWRIGHT_HTML_REPORT` environment variable or a reporter configuration. +that location using the `PLAYWRIGHT_HTML_OUTPUT_DIR` environment variable or a reporter configuration. In configuration file, pass options directly: @@ -207,6 +233,16 @@ Or if there is a custom folder name: npx playwright show-report my-report ``` +HTML report supports the following configuration options and environment variables: + +| Environment Variable Name | Reporter Config Option| Description | Default +|---|---|---|---| +| `PLAYWRIGHT_HTML_OUTPUT_DIR` | `outputFolder` | Directory to save the report to. | `playwright-report` +| `PLAYWRIGHT_HTML_OPEN` | `open` | When to open the html report in the browser, one of `'always'`, `'never'` or `'on-failure'` | `'on-failure'` +| `PLAYWRIGHT_HTML_HOST` | `host` | When report opens in the browser, it will be served bound to this hostname. | `localhost` +| `PLAYWRIGHT_HTML_PORT` | `port` | When report opens in the browser, it will be served on this port. | `9323` or any available port when `9323` is not available. +| `PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL` | `attachmentsBaseURL` | A separate location where attachments from the `data` subdirectory are uploaded. Only needed when you upload report and `data` separately to different locations. | `data/` + ### Blob reporter Blob reports contain all the details about the test run and can be used later to produce any other report. Their primary function is to facilitate the merging of reports from [sharded tests](./test-sharding.md). @@ -308,8 +344,8 @@ JUnit report supports following configuration options and environment variables: | `PLAYWRIGHT_JUNIT_OUTPUT_DIR` | | Directory to save the output file. Ignored if output file is not specified. | `cwd` or config directory. | `PLAYWRIGHT_JUNIT_OUTPUT_NAME` | `outputFile` | Base file name for the output, relative to the output dir. | JUnit report is printed to the stdout. | `PLAYWRIGHT_JUNIT_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_JUNIT_OUTPUT_DIR` and `PLAYWRIGHT_JUNIT_OUTPUT_NAME` will be ignored. | JUnit report is printed to the stdout. -| | `stripANSIControlSequences` | Whether to remove ANSI control sequences from the text before writing it in the report. | By default output text is added as is. -| | `includeProjectInTestName` | Whether to include Playwright project name in every test case as a name prefix. | By default not included. +| `PLAYWRIGHT_JUNIT_STRIP_ANSI` | `stripANSIControlSequences` | Whether to remove ANSI control sequences from the text before writing it in the report. | By default output text is added as is. +| `PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME` | `includeProjectInTestName` | Whether to include Playwright project name in every test case as a name prefix. | By default not included. | `PLAYWRIGHT_JUNIT_SUITE_ID` | | Value of the `id` attribute on the root `` report entry. | Empty string. | `PLAYWRIGHT_JUNIT_SUITE_NAME` | | Value of the `name` attribute on the root `` report entry. | Empty string. diff --git a/packages/playwright-core/src/utils/env.ts b/packages/playwright-core/src/utils/env.ts index 3a13296d8f..2a4dd0bfd4 100644 --- a/packages/playwright-core/src/utils/env.ts +++ b/packages/playwright-core/src/utils/env.ts @@ -21,9 +21,13 @@ export function getFromENV(name: string): string | undefined { return value; } -export function getAsBooleanFromENV(name: string): boolean { +export function getAsBooleanFromENV(name: string, defaultValue?: boolean | undefined): boolean { const value = getFromENV(name); - return !!value && value !== 'false' && value !== '0'; + if (value === 'false' || value === '0') + return false; + if (value) + return true; + return !!defaultValue; } export function getPackageManager() { diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 2f152f447e..2a5bbc94d4 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -62,14 +62,14 @@ class HtmlReporter extends EmptyReporter { private _outputFolder!: string; private _attachmentsBaseURL!: string; private _open: string | undefined; + private _port: number | undefined; + private _host: string | undefined; private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined; private _topLevelErrors: TestError[] = []; constructor(options: HtmlReporterOptions) { super(); this._options = options; - if (options._mode === 'test') - process.env.PW_HTML_REPORT = '1'; } override printsToStdio() { @@ -81,9 +81,11 @@ class HtmlReporter extends EmptyReporter { } override onBegin(suite: Suite) { - const { outputFolder, open, attachmentsBaseURL } = this._resolveOptions(); + const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions(); this._outputFolder = outputFolder; this._open = open; + this._host = host; + this._port = port; this._attachmentsBaseURL = attachmentsBaseURL; const reportedWarnings = new Set(); for (const project of this.config.projects) { @@ -104,12 +106,14 @@ class HtmlReporter extends EmptyReporter { this.suite = suite; } - _resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string } { + _resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string, host: string | undefined, port: number | undefined } { const outputFolder = reportFolderFromEnv() ?? resolveReporterOutputPath('playwright-report', this._options.configDir, this._options.outputFolder); return { outputFolder, open: getHtmlReportOptionProcessEnv() || this._options.open || 'on-failure', - attachmentsBaseURL: this._options.attachmentsBaseURL || 'data/' + attachmentsBaseURL: process.env.PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL || this._options.attachmentsBaseURL || 'data/', + host: process.env.PLAYWRIGHT_HTML_HOST || this._options.host, + port: process.env.PLAYWRIGHT_HTML_PORT ? +process.env.PLAYWRIGHT_HTML_PORT : this._options.port, }; } @@ -135,12 +139,12 @@ class HtmlReporter extends EmptyReporter { const { ok, singleTestId } = this._buildResult; const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure')); if (shouldOpen) { - await showHTMLReport(this._outputFolder, this._options.host, this._options.port, singleTestId); + await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId); } else if (this._options._mode === 'test') { const packageManagerCommand = getPackageManagerExecCommand(); const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder); - const hostArg = this._options.host ? ` --host ${this._options.host}` : ''; - const portArg = this._options.port ? ` --port ${this._options.port}` : ''; + const hostArg = this._host ? ` --host ${this._host}` : ''; + const portArg = this._port ? ` --port ${this._port}` : ''; console.log(''); console.log('To open last HTML report run:'); console.log(colors.cyan(` @@ -151,18 +155,18 @@ class HtmlReporter extends EmptyReporter { } function reportFolderFromEnv(): string | undefined { - if (process.env[`PLAYWRIGHT_HTML_REPORT`]) - return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_HTML_REPORT`]); - return undefined; + // Note: PLAYWRIGHT_HTML_REPORT is for backwards compatibility. + const envValue = process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || process.env.PLAYWRIGHT_HTML_REPORT; + return envValue ? path.resolve(envValue) : undefined; } function getHtmlReportOptionProcessEnv(): HtmlReportOpenOption | undefined { - const processKey = 'PW_TEST_HTML_REPORT_OPEN'; - const htmlOpenEnv = process.env[processKey]; + // Note: PW_TEST_HTML_REPORT_OPEN is for backwards compatibility. + const htmlOpenEnv = process.env.PLAYWRIGHT_HTML_OPEN || process.env.PW_TEST_HTML_REPORT_OPEN; if (!htmlOpenEnv) return undefined; if (!isHtmlReportOption(htmlOpenEnv)) { - console.log(colors.red(`Configuration Error: HTML reporter Invalid value for ${processKey}: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(', ')}`)); + console.log(colors.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(', ')}`)); return undefined; } return htmlOpenEnv; diff --git a/packages/playwright/src/reporters/junit.ts b/packages/playwright/src/reporters/junit.ts index 1f0cbf6362..61cf7e8e99 100644 --- a/packages/playwright/src/reporters/junit.ts +++ b/packages/playwright/src/reporters/junit.ts @@ -19,6 +19,7 @@ import path from 'path'; import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter'; import { formatFailure, resolveOutputFile, stripAnsiEscapes } from './base'; import EmptyReporter from './empty'; +import { getAsBooleanFromENV } from 'playwright-core/lib/utils'; type JUnitOptions = { outputFile?: string, @@ -42,8 +43,8 @@ class JUnitReporter extends EmptyReporter { constructor(options: JUnitOptions) { super(); - this.stripANSIControlSequences = options.stripANSIControlSequences || false; - this.includeProjectInTestName = options.includeProjectInTestName || false; + this.stripANSIControlSequences = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_STRIP_ANSI', !!options.stripANSIControlSequences); + this.includeProjectInTestName = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME', !!options.includeProjectInTestName); this.configDir = options.configDir; this.resolvedOutputFile = resolveOutputFile('JUNIT', options)?.outputFile; } diff --git a/packages/playwright/src/reporters/list.ts b/packages/playwright/src/reporters/list.ts index 65057e16f9..94e507fd4b 100644 --- a/packages/playwright/src/reporters/list.ts +++ b/packages/playwright/src/reporters/list.ts @@ -17,6 +17,7 @@ import { ms as milliseconds } from 'playwright-core/lib/utilsBundle'; import { colors, BaseReporter, formatError, formatTestTitle, isTTY, stepSuffix, stripAnsiEscapes, ttyWidth } from './base'; import type { FullResult, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter'; +import { getAsBooleanFromENV } from 'playwright-core/lib/utils'; // Allow it in the Visual Studio Code Terminal and the new Windows Terminal const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION; @@ -33,9 +34,9 @@ class ListReporter extends BaseReporter { private _needNewLine = false; private _printSteps: boolean; - constructor(options: { omitFailures?: boolean, printSteps?: boolean } = {}) { - super(options); - this._printSteps = isTTY && (options.printSteps || !!process.env.PW_TEST_DEBUG_REPORTERS_PRINT_STEPS); + constructor(options: { printSteps?: boolean } = {}) { + super(); + this._printSteps = isTTY && getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps); } override printsToStdio() { diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts index 7850a52f07..f8619dd6fe 100644 --- a/tests/playwright-test/playwright-test-fixtures.ts +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -224,6 +224,7 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { GITHUB_SHA: undefined, // END: Reserved CI PW_TEST_HTML_REPORT_OPEN: undefined, + PLAYWRIGHT_HTML_OPEN: undefined, PW_TEST_REPORTER: undefined, PW_TEST_REPORTER_WS_ENDPOINT: undefined, PW_TEST_SOURCE_TRANSFORM: undefined, diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 5dfd3c7524..61abc43628 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -209,7 +209,7 @@ test('should merge into html with dependencies', async ({ runInlineTest, mergeRe const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/)]); - const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode, output } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); expect(output).not.toContain('To open last HTML report run:'); @@ -280,7 +280,7 @@ test('should merge blob into blob', async ({ runInlineTest, mergeReports, showRe } { const compinedBlobReportDir = test.info().outputPath('blob-report'); - const { exitCode } = await mergeReports(compinedBlobReportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html,json'] }); + const { exitCode } = await mergeReports(compinedBlobReportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html,json'] }); expect(exitCode).toBe(0); expect(fs.existsSync(test.info().outputPath('report.json'))).toBe(true); await showReport(); @@ -335,7 +335,7 @@ test('be able to merge incomplete shards', async ({ runInlineTest, mergeReports, const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/)]); - const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); await showReport(); @@ -374,7 +374,7 @@ test('total time is from test run not from merge', async ({ runInlineTest, merge await runInlineTest(files, { shard: `1/2` }); await runInlineTest(files, { shard: `2/2` }, { PWTEST_BLOB_DO_NOT_REMOVE: '1' }); - const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode, output } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); expect(output).not.toContain('To open last HTML report run:'); @@ -441,7 +441,7 @@ test('merge into list report by default', async ({ runInlineTest, mergeReports } const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip', 'report-3.zip']); - const { exitCode, output } = await mergeReports(reportDir, { PW_TEST_DEBUG_REPORTERS: '1', PW_TEST_DEBUG_REPORTERS_PRINT_STEPS: '1', PLAYWRIGHT_FORCE_TTY: '80' }, { additionalArgs: ['--reporter', 'list'] }); + const { exitCode, output } = await mergeReports(reportDir, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_LIST_PRINT_STEPS: '1', PLAYWRIGHT_FORCE_TTY: '80' }, { additionalArgs: ['--reporter', 'list'] }); expect(exitCode).toBe(0); const text = stripAnsi(output); @@ -520,7 +520,7 @@ test('should print progress', async ({ runInlineTest, mergeReports }) => { const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip']); - const { exitCode, output } = await mergeReports(reportDir, { PW_TEST_HTML_REPORT_OPEN: 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode, output } = await mergeReports(reportDir, { PLAYWRIGHT_HTML_OPEN: 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); const lines = output.split('\n'); @@ -571,7 +571,7 @@ test('preserve attachments', async ({ runInlineTest, mergeReports, showReport, p const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual(['report-1.zip']); - const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); await showReport(); @@ -634,7 +634,7 @@ test('generate html with attachment urls', async ({ runInlineTest, mergeReports, const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual(['report-1.zip']); - const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); const htmlReportDir = test.info().outputPath('playwright-report'); @@ -709,7 +709,7 @@ test('resource names should not clash between runs', async ({ runInlineTest, sho reportFiles.sort(); expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip']); - const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); await showReport(); @@ -781,7 +781,7 @@ test('multiple output reports', async ({ runInlineTest, mergeReports, showReport const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual(['report-1.zip']); - const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html,line'] }); + const { exitCode, output } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html,line'] }); expect(exitCode).toBe(0); // Check that line reporter was called. @@ -907,7 +907,7 @@ test('onError in the report', async ({ runInlineTest, mergeReports, showReport, const result = await runInlineTest(files, { shard: `1/3` }, { PWTEST_BOT_NAME: 'macos-node16-ttest' }); expect(result.exitCode).toBe(1); - const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); await showReport(); @@ -1149,7 +1149,7 @@ test('preserve steps in html report', async ({ runInlineTest, mergeReports, show // relative to the current directory. const mergeCwd = test.info().outputPath('foo'); await fs.promises.mkdir(mergeCwd, { recursive: true }); - const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'], cwd: mergeCwd }); + const { exitCode, output } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'], cwd: mergeCwd }); expect(exitCode).toBe(0); expect(output).not.toContain('To open last HTML report run:'); @@ -1326,7 +1326,7 @@ test('keep projects with same name different bot name separate', async ({ runInl await runInlineTest(files('second'), undefined, { PWTEST_BOT_NAME: 'second', PWTEST_BLOB_DO_NOT_REMOVE: '1' }); const reportDir = test.info().outputPath('blob-report'); - const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); await showReport(); await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('1'); @@ -1452,7 +1452,7 @@ test('merge-reports should throw if report version is from the future', async ({ await fs.promises.rm(reportZipFile, { force: true }); await zipReport(events, reportZipFile); - const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode, output } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(1); expect(output).toContain(`Error: Blob report report-2.zip was created with a newer version of Playwright.`); @@ -1496,7 +1496,7 @@ test('should merge blob reports with same name', async ({ runInlineTest, mergeRe await fs.promises.cp(reportZip, path.join(allReportsDir, 'report-1.zip')); await fs.promises.cp(reportZip, path.join(allReportsDir, 'report-2.zip')); - const { exitCode } = await mergeReports(allReportsDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode } = await mergeReports(allReportsDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); await showReport(); await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('10'); @@ -1887,7 +1887,7 @@ test('preserve static annotations when tests did not run', async ({ runInlineTes ` }; await runInlineTest(files); - const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); + const { exitCode } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); await showReport(); diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index e10302c4a1..8283ecb5fe 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -65,7 +65,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(testInfo.retry).toBe(1); }); `, - }, { reporter: 'dot,html', retries: 1 }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html', retries: 1 }, { PLAYWRIGHT_HTML_OPEN: 'never' }); await showReport(); @@ -95,6 +95,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect(1).toBe(1); }); `, + // Note: using PW_TEST_HTML_REPORT_OPEN to test backwards compatibility. }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -113,7 +114,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect(page.locator('.attachment-body')).toHaveText(/TESTID=.*/); }); - test('should not throw when PW_TEST_HTML_REPORT_OPEN value is invalid', async ({ runInlineTest, page, showReport }, testInfo) => { + test('should not throw when PLAYWRIGHT_HTML_OPEN value is invalid', async ({ runInlineTest, page, showReport }, testInfo) => { const invalidOption = 'invalid-option'; const result = await runInlineTest({ 'playwright.config.ts': ` @@ -125,7 +126,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(2).toEqual(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: invalidOption }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: invalidOption }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); }); @@ -143,7 +144,7 @@ for (const useIntermediateMergeReport of [false] as const) { testInfo.attachments.push({ name: 'screenshot', path: screenshot, contentType: 'image/png' }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -169,7 +170,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect(screenshot).toMatchSnapshot('expected.png'); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); @@ -243,7 +244,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect.soft(page).toHaveScreenshot({ timeout: 1000 }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); @@ -278,7 +279,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect.soft(screenshot).toMatchSnapshot('expected.png'); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); @@ -309,7 +310,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect.soft(page).toHaveScreenshot({ timeout: 1000 }); }); `, - }, { 'reporter': 'dot,html', 'update-snapshots': true }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { 'reporter': 'dot,html', 'update-snapshots': true }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); @@ -344,7 +345,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect(screenshot).toMatchSnapshot('expected'); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); @@ -373,7 +374,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect(true).toBeFalsy(); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); @@ -404,7 +405,7 @@ for (const useIntermediateMergeReport of [false] as const) { await page.evaluate('2 + 2'); }); ` - }, {}, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, {}, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -432,7 +433,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect(true).toBeFalsy(); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); @@ -452,7 +453,7 @@ for (const useIntermediateMergeReport of [false] as const) { await expect(true).toBeFalsy(); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); @@ -475,7 +476,7 @@ for (const useIntermediateMergeReport of [false] as const) { await evaluateWrapper(page, '2 + 2'); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -508,7 +509,7 @@ for (const useIntermediateMergeReport of [false] as const) { await page.evaluate('2 + 2'); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -538,7 +539,7 @@ for (const useIntermediateMergeReport of [false] as const) { await page.evaluate('2 + 2'); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -562,7 +563,7 @@ for (const useIntermediateMergeReport of [false] as const) { await request.dispose(); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -600,7 +601,7 @@ for (const useIntermediateMergeReport of [false] as const) { ]); }); `, - }, { reporter: 'html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -625,7 +626,7 @@ for (const useIntermediateMergeReport of [false] as const) { await page.evaluate('2 + 2'); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -683,7 +684,7 @@ for (const useIntermediateMergeReport of [false] as const) { }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(0); @@ -726,7 +727,7 @@ for (const useIntermediateMergeReport of [false] as const) { test.info().annotations.push({ type: 'issue', description: 'I am not interested in this test' }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -746,7 +747,7 @@ for (const useIntermediateMergeReport of [false] as const) { test.info().annotations.push({ type: 'issue', description: '${server.EMPTY_PAGE}' }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -789,7 +790,7 @@ for (const useIntermediateMergeReport of [false] as const) { }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); await showReport(); @@ -814,7 +815,7 @@ for (const useIntermediateMergeReport of [false] as const) { await testInfo.attach('example.ext with spaces', { body: Buffer.from('b'), contentType: 'madeup' }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); await showReport(); await page.getByRole('link', { name: 'passing' }).click(); @@ -867,7 +868,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect('new').toMatchSnapshot('snapshot.txt'); }); ` - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); await showReport(); await page.click('text="is a test"'); @@ -894,7 +895,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect('newcommon').toMatchSnapshot('snapshot.txt'); }); ` - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); await showReport(); await page.click('text="is a test"'); @@ -912,7 +913,7 @@ for (const useIntermediateMergeReport of [false] as const) { throw new Error('ouch'); }); ` - }, { 'reporter': 'dot,html', 'repeat-each': 3 }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { 'reporter': 'dot,html', 'repeat-each': 3 }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); await showReport(); @@ -937,7 +938,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(2).toEqual(2); }); ` - }, { 'reporter': 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { 'reporter': 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); await showReport(); @@ -956,7 +957,7 @@ for (const useIntermediateMergeReport of [false] as const) { test('sample', async ({}) => { expect(2).toBe(2); }); `, 'a.spec.js': `require('./inner')` - }, { 'reporter': 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { 'reporter': 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); await showReport(); await expect(page.locator('text=a.spec.js')).toBeVisible(); @@ -997,7 +998,7 @@ for (const useIntermediateMergeReport of [false] as const) { await execGit(['commit', '-m', 'awesome commit message']); const result = await runInlineTest(files, { reporter: 'dot,html' }, { - PW_TEST_HTML_REPORT_OPEN: 'never', + PLAYWRIGHT_HTML_OPEN: 'never', GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test', GITHUB_RUN_ID: 'example-run-id', GITHUB_SERVER_URL: 'https://playwright.dev', @@ -1043,7 +1044,7 @@ for (const useIntermediateMergeReport of [false] as const) { import { test, expect } from '@playwright/test'; test('sample', async ({}) => { expect(2).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never', GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test', GITHUB_RUN_ID: 'example-run-id', GITHUB_SERVER_URL: 'https://playwright.dev', GITHUB_SHA: 'example-sha' }, undefined); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never', GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test', GITHUB_RUN_ID: 'example-run-id', GITHUB_SERVER_URL: 'https://playwright.dev', GITHUB_SHA: 'example-sha' }, undefined); await showReport(); @@ -1071,7 +1072,7 @@ for (const useIntermediateMergeReport of [false] as const) { import { test, expect } from '@playwright/test'; test('my sample test', async ({}) => { expect(2).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }, undefined); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }, undefined); await showReport(); @@ -1095,7 +1096,7 @@ for (const useIntermediateMergeReport of [false] as const) { import { test, expect } from '@playwright/test'; test('my sample test', async ({}) => { expect(2).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); await showReport(); @@ -1177,7 +1178,7 @@ for (const useIntermediateMergeReport of [false] as const) { test('pass', ({}, testInfo) => { }); ` - }, { 'reporter': 'html,line' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }, { + }, { 'reporter': 'html,line' }, { PLAYWRIGHT_HTML_OPEN: 'never' }, { cwd: 'foo/bar/baz/tests', }); expect(result.exitCode).toBe(0); @@ -1201,7 +1202,7 @@ for (const useIntermediateMergeReport of [false] as const) { test('pass', ({}, testInfo) => { }); ` - }, { 'reporter': 'html,line' }, { 'PW_TEST_HTML_REPORT_OPEN': 'never', 'PLAYWRIGHT_HTML_REPORT': '../my-report' }, { + }, { 'reporter': 'html,line' }, { 'PLAYWRIGHT_HTML_OPEN': 'never', 'PLAYWRIGHT_HTML_OUTPUT_DIR': '../my-report' }, { cwd: 'foo/bar/baz/tests', }); expect(result.exitCode).toBe(0); @@ -1250,7 +1251,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(3); @@ -1324,7 +1325,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(1); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(3); @@ -1366,7 +1367,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(1); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(3); @@ -1407,7 +1408,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(1); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(3); @@ -1445,7 +1446,7 @@ for (const useIntermediateMergeReport of [false] as const) { }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(2); @@ -1523,7 +1524,7 @@ for (const useIntermediateMergeReport of [false] as const) { }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(5); @@ -1566,7 +1567,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(1); @@ -1615,7 +1616,7 @@ for (const useIntermediateMergeReport of [false] as const) { }); } `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(7); @@ -1689,7 +1690,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(2); @@ -1755,7 +1756,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(2); @@ -1804,7 +1805,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(1); @@ -1882,7 +1883,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(1); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(3); @@ -2021,7 +2022,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(0); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(3); @@ -2110,7 +2111,7 @@ for (const useIntermediateMergeReport of [false] as const) { test('passes', () => {}); } `, - }, { reporter: 'html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); await showReport(); @@ -2139,7 +2140,7 @@ for (const useIntermediateMergeReport of [false] as const) { }); test('test 6', async ({}) => {}); `, - }, { reporter: 'html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); await showReport(); @@ -2167,7 +2168,7 @@ for (const useIntermediateMergeReport of [false] as const) { test('b test 1', async ({}) => {}); test('b test 2', async ({}) => {}); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(4); @@ -2197,7 +2198,7 @@ for (const useIntermediateMergeReport of [false] as const) { test('failed title', async ({}) => { expect(1).toBe(1); }); test('passes title', async ({}) => { expect(1).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(1); @@ -2220,7 +2221,7 @@ for (const useIntermediateMergeReport of [false] as const) { test('test1', async ({}) => { expect(1).toBe(1); }); test('test2', async ({}) => { expect(1).toBe(2); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(1); @@ -2257,7 +2258,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(1); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -2286,7 +2287,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(1); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -2315,7 +2316,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(1); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -2344,7 +2345,7 @@ for (const useIntermediateMergeReport of [false] as const) { expect(1).toBe(1); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -2374,7 +2375,7 @@ for (const useIntermediateMergeReport of [false] as const) { 'playwright.config.ts': ` export default { globalTeardown: './globalTeardown.ts' }; `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(1); @@ -2395,7 +2396,7 @@ for (const useIntermediateMergeReport of [false] as const) { }); }); `, - }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); diff --git a/tests/playwright-test/reporter-json.spec.ts b/tests/playwright-test/reporter-json.spec.ts index 313cd0e71e..f8ec8b9b0b 100644 --- a/tests/playwright-test/reporter-json.spec.ts +++ b/tests/playwright-test/reporter-json.spec.ts @@ -281,7 +281,7 @@ test.describe('report location', () => { test('pass', ({}, testInfo) => { }); ` - }, { 'reporter': 'json' }, { 'PW_TEST_HTML_REPORT_OPEN': 'never', 'PLAYWRIGHT_JSON_OUTPUT_NAME': '../my-report.json' }, { + }, { 'reporter': 'json' }, { 'PLAYWRIGHT_JSON_OUTPUT_NAME': '../my-report.json' }, { cwd: 'foo/bar/baz/tests', }); expect(result.exitCode).toBe(0); @@ -302,7 +302,7 @@ test.describe('report location', () => { test('pass', ({}, testInfo) => { }); ` - }, { 'reporter': 'json' }, { 'PW_TEST_HTML_REPORT_OPEN': 'never', 'PLAYWRIGHT_JSON_OUTPUT_FILE': '../my-report.json' }, { + }, { 'reporter': 'json' }, { 'PLAYWRIGHT_JSON_OUTPUT_FILE': '../my-report.json' }, { cwd: 'foo/bar/baz/tests', }); expect(result.exitCode).toBe(0); diff --git a/tests/playwright-test/reporter-list.spec.ts b/tests/playwright-test/reporter-list.spec.ts index 2b924e5986..489e97feb7 100644 --- a/tests/playwright-test/reporter-list.spec.ts +++ b/tests/playwright-test/reporter-list.spec.ts @@ -70,7 +70,7 @@ for (const useIntermediateMergeReport of [false, true] as const) { }); }); `, - }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PW_TEST_DEBUG_REPORTERS_PRINT_STEPS: '1', PLAYWRIGHT_FORCE_TTY: '80' }); + }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_LIST_PRINT_STEPS: '1', PLAYWRIGHT_FORCE_TTY: '80' }); const text = result.output; const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); lines.pop(); // Remove last item that contains [v] and time in ms. From 68abd36464e00155d249ecc690435a6448d5a65e Mon Sep 17 00:00:00 2001 From: Matt Marsh Date: Tue, 21 May 2024 20:06:05 +0100 Subject: [PATCH 25/32] docs: add detail on dot reporter output (#30939) docs: detail on how to interpret dot reporter output added. Fixes #30908 --- docs/src/test-reporters-js.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md index 7b0f91fea4..b131a9a411 100644 --- a/docs/src/test-reporters-js.md +++ b/docs/src/test-reporters-js.md @@ -167,6 +167,16 @@ Running 124 tests using 6 workers ······F············································· ``` +One character is displayed for each test that has run, indicating its status: + +| Character | Description +|---|---| +| `·` | Passed +| `F` | Failed +| `×` | Failed or timed out - and will be retried +| `±` | Passed on retry (flaky) +| `T` | Timed out +| `°` | Skipped Dot report supports the following configuration options and environment variables: @@ -175,7 +185,6 @@ Dot report supports the following configuration options and environment variable | `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. | `true` when terminal is in TTY mode, `false` otherwise. | `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise. - ### HTML reporter HTML reporter produces a self-contained folder that contains report for the test run that can be served as a web page. From d0644f5444a00531517a2cd0ffec0a1364e6bdf6 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Tue, 21 May 2024 15:15:05 -0400 Subject: [PATCH 26/32] fix(electron): flaky startup if stderr comes in too fast (#30855) Chromium's `DevTools listening on` message sometimes arrives before Playwright is finished connecting to Node. Without this patch, it would miss the message and fail to connect. --- .../playwright-core/src/server/electron/electron.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index 9b934ca2f7..b8f361b48a 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -229,6 +229,8 @@ export class Electron extends SdkObject { onExit: () => app?.emit(ElectronApplication.Events.Close), }); + // All waitForLines must be started immediately. + // Otherwise the lines might come before we are ready. const waitForXserverError = new Promise(async (resolve, reject) => { waitForLine(progress, launchedProcess, /Unable to open X display/).then(() => reject(new Error([ 'Unable to open X display!', @@ -240,17 +242,20 @@ export class Electron extends SdkObject { progress.metadata.log ].join('\n')))).catch(() => {}); }); + const nodeMatchPromise = waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/); + const chromeMatchPromise = waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/); + const debuggerDisconnectPromise = waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./); - const nodeMatch = await waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/); + const nodeMatch = await nodeMatchPromise; const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]); const nodeConnection = new CRConnection(nodeTransport, helper.debugProtocolLogger(), browserLogsCollector); // Immediately release exiting process under debug. - waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./).then(() => { + debuggerDisconnectPromise.then(() => { nodeTransport.close(); }).catch(() => {}); const chromeMatch = await Promise.race([ - waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/), + chromeMatchPromise, waitForXserverError, ]) as RegExpMatchArray; const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]); From 822cba2e2b9a7ff8a48cce9bbf9f3a023175405b Mon Sep 17 00:00:00 2001 From: Cornelius Roemer Date: Tue, 21 May 2024 21:35:17 +0200 Subject: [PATCH 27/32] docs(actionability.md): fix grammar (#30756) --- docs/src/actionability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/actionability.md b/docs/src/actionability.md index 337e5fd344..4a84f5f5b2 100644 --- a/docs/src/actionability.md +++ b/docs/src/actionability.md @@ -9,7 +9,7 @@ Playwright performs a range of actionability checks on the elements before makin behave as expected. It auto-waits for all the relevant checks to pass and only then performs the requested action. If the required checks do not pass within the given `timeout`, action fails with the `TimeoutError`. For example, for [`method: Locator.click`], Playwright will ensure that: -- locator resolves to an exactly one element +- locator resolves to exactly one element - element is [Visible] - element is [Stable], as in not animating or completed animation - element [Receives Events], as in not obscured by other elements From 47185b743b059814d6a7312c03fcc29dbff2d7ba Mon Sep 17 00:00:00 2001 From: Debbie O'Brien Date: Tue, 21 May 2024 21:37:39 +0200 Subject: [PATCH 28/32] docs: add last failed to running tests (#30730) --- docs/src/running-tests-js.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/running-tests-js.md b/docs/src/running-tests-js.md index 77d201338d..71d8b8adb5 100644 --- a/docs/src/running-tests-js.md +++ b/docs/src/running-tests-js.md @@ -83,6 +83,15 @@ To run a test with a specific title, use the `-g` flag followed by the title of npx playwright test -g "add a todo item" ``` +### Run last failed tests + +To run only the tests that failed in the last test run, first run your tests and then run them again with the `--last-failed` flag. + +```bash +npx playwright test --last-failed +``` + + ### Run tests in VS Code Tests can be run right from VS Code using the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). Once installed you can simply click the green triangle next to the test you want to run or run all tests from the testing sidebar. Check out our [Getting Started with VS Code](./getting-started-vscode.md#running-tests) guide for more details. From 165ecac5df9bf4472b3c37d5b3b6e4093b7d1f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=86=8C=ED=98=84?= <53892427+osohyun0224@users.noreply.github.com> Date: Wed, 22 May 2024 04:46:38 +0900 Subject: [PATCH 29/32] feat(test): add `URL` field to annotations for hyperlink display (#30665) --- docs/src/test-api/class-test.md | 18 ++++++++++++++++-- .../html-reporter/src/testCaseView.spec.tsx | 4 ++-- packages/html-reporter/src/types.ts | 2 +- packages/playwright/src/common/config.ts | 2 +- packages/playwright/types/test.d.ts | 6 ++++-- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 9b56fbf2e6..7b03353361 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -71,7 +71,8 @@ import { test, expect } from '@playwright/test'; test('basic test', { annotation: { type: 'issue', - description: 'https://github.com/microsoft/playwright/issues/23180', + description: 'feature tags API', + url: 'https://github.com/microsoft/playwright/issues/23180' }, }, async ({ page }) => { await page.goto('https://playwright.dev/'); @@ -97,7 +98,8 @@ Test title. - `tag` ?<[string]|[Array]<[string]>> - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> Annotation type, for example `'issue'`. - - `description` ?<[string]> Optional annotation description, for example an issue url. + - `description` ?<[string]> Optional annotation description. + - `url` ?<[string]> Optional for example an issue url. Additional test details. @@ -440,6 +442,7 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> Additional details for all tests in the group. @@ -568,6 +571,7 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -623,6 +627,7 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -676,6 +681,7 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -727,6 +733,7 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -782,6 +789,7 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -839,6 +847,7 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -891,6 +900,7 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -1109,6 +1119,7 @@ Test title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.(call)`] for test details description. @@ -1214,6 +1225,7 @@ Test title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.(call)`] for test details description. @@ -1291,6 +1303,7 @@ Test title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.(call)`] for test details description. @@ -1436,6 +1449,7 @@ Test title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> + - `url` ?<[string]> See [`method: Test.(call)`] for test details description. diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index 28fe8247f5..0a4da91b6e 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -52,8 +52,8 @@ const testCase: TestCase = { projectName: 'chromium', location: { file: 'test.spec.ts', line: 42, column: 0 }, annotations: [ - { type: 'annotation', description: 'Annotation text' }, - { type: 'annotation', description: 'Another annotation text' }, + { type: 'annotation', description: 'Annotation text', url: 'example url' }, + { type: 'annotation', description: 'Another annotation text', url: 'Another example url' }, ], tags: [], outcome: 'expected', diff --git a/packages/html-reporter/src/types.ts b/packages/html-reporter/src/types.ts index 733e88e8b9..79dc2f3812 100644 --- a/packages/html-reporter/src/types.ts +++ b/packages/html-reporter/src/types.ts @@ -59,7 +59,7 @@ export type TestFileSummary = { stats: Stats; }; -export type TestCaseAnnotation = { type: string, description?: string }; +export type TestCaseAnnotation = { type: string, description?: string, url?: string}; export type TestCaseSummary = { testId: string, diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 42dd554b38..3af70211bc 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -35,7 +35,7 @@ export type FixturesWithLocation = { fixtures: Fixtures; location: Location; }; -export type Annotation = { type: string, description?: string }; +export type Annotation = { type: string, description?: string, url?: string }; export const defaultTimeout = 30000; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 9e050e2901..444f135a89 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -2156,7 +2156,8 @@ interface TestFunction { * test('basic test', { * annotation: { * type: 'issue', - * description: 'https://github.com/microsoft/playwright/issues/23180', + * description: 'feature tags API', + * url: 'https://github.com/microsoft/playwright/issues/23180' * }, * }, async ({ page }) => { * await page.goto('https://playwright.dev/'); @@ -2232,7 +2233,8 @@ interface TestFunction { * test('basic test', { * annotation: { * type: 'issue', - * description: 'https://github.com/microsoft/playwright/issues/23180', + * description: 'feature tags API', + * url: 'https://github.com/microsoft/playwright/issues/23180' * }, * }, async ({ page }) => { * await page.goto('https://playwright.dev/'); From 148d759a4c694ffd48852f4c8c7d37d47518e46e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 21 May 2024 12:49:47 -0700 Subject: [PATCH 30/32] fix(chromium): do not fetch intercepted request body from network (#30938) Fixes https://github.com/microsoft/playwright/issues/30760 --- .../src/server/chromium/crNetworkManager.ts | 6 +++++ tests/page/page-request-fulfill.spec.ts | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index 6411457a32..3752a5c06f 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -376,6 +376,10 @@ export class CRNetworkManager { if (response.body || !expectedLength) return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); + // Make sure no network requests sent while reading the body for fulfilled requests. + if (request._route?._fulfilled) + return Buffer.from(''); + // For hello, world!`); }); + +it('should not go to the network for fulfilled requests body', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30760' }, +}, async ({ page, server, browserName }) => { + await page.route('**/one-style.css', async route => { + return route.fulfill({ + status: 404, + contentType: 'text/plain', + body: 'Not Found! (mocked)', + }); + }); + + let serverHit = false; + server.setRoute('/one-style.css', (req, res) => { + serverHit = true; + res.setHeader('Content-Type', 'text/css'); + res.end('body { background-color: green; }'); + }); + + const responsePromise = page.waitForResponse('**/one-style.css'); + await page.goto(server.PREFIX + '/one-style.html'); + const response = await responsePromise; + const body = await response.body(); + expect(body).toBeTruthy(); + expect(serverHit).toBe(false); +}); + From 7b27fc3916030d1a0892a0cc526a0d6b99a673c6 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 21 May 2024 14:36:31 -0700 Subject: [PATCH 31/32] chore: pass outputDir to uimode (#30941) Fixes https://github.com/microsoft/playwright/issues/30886 --- .../playwright-core/src/server/trace/viewer/traceViewer.ts | 3 +++ packages/playwright/src/isomorphic/testServerInterface.ts | 1 + packages/playwright/src/program.ts | 1 + packages/playwright/src/runner/testServer.ts | 1 + packages/trace-viewer/src/ui/uiModeView.tsx | 2 ++ 5 files changed, 8 insertions(+) diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 741a927757..3c2872e6fa 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -46,6 +46,7 @@ export type TraceViewerRedirectOptions = { reporter?: string[]; webApp?: string; isServer?: boolean; + outputDir?: string; }; export type TraceViewerAppOptions = { @@ -129,6 +130,8 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ params.append('timeout', String(options.timeout)); if (options.headed) params.append('headed', ''); + if (options.outputDir) + params.append('outputDir', options.outputDir); for (const reporter of options.reporter || []) params.append('reporter', reporter); diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 4abf3f02fe..c63a9793d3 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -92,6 +92,7 @@ export interface TestServerInterface { headed?: boolean; workers?: number | string; timeout?: number, + outputDir?: string; reporters?: string[], trace?: 'on' | 'off'; video?: 'on' | 'off'; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index b69d3d6b10..f462b81d43 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -170,6 +170,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { reporter: Array.isArray(opts.reporter) ? opts.reporter : opts.reporter ? [opts.reporter] : undefined, workers: cliOverrides.workers, timeout: cliOverrides.timeout, + outputDir: cliOverrides.outputDir, }); await stopProfiling('runner'); if (status === 'restarted') diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index d2806b03f6..e797ed9b08 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -313,6 +313,7 @@ class TestServerDispatcher implements TestServerInterface { _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined, _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined, }, + outputDir: params.outputDir, workers: params.workers, }; if (params.trace === 'on') diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 9c067dbc89..914280cb34 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -60,6 +60,7 @@ const queryParams = { workers: searchParams.get('workers') || undefined, timeout: searchParams.has('timeout') ? +searchParams.get('timeout')! : undefined, headed: searchParams.has('headed'), + outputDir: searchParams.get('outputDir') || undefined, reporters: searchParams.has('reporter') ? searchParams.getAll('reporter') : undefined, }; @@ -283,6 +284,7 @@ export const UIModeView: React.FC<{}> = ({ workers: queryParams.workers, timeout: queryParams.timeout, headed: queryParams.headed, + outputDir: queryParams.outputDir, reporters: queryParams.reporters, trace: 'on', }); From e7a11c0ca22881623b073f2540c21b5d2da24085 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 21 May 2024 18:05:58 -0700 Subject: [PATCH 32/32] fix: propagate close reason to api context (#30945) --- examples/todomvc/tests/integration.spec.ts | 69 +++++++++++++++++++ .../src/client/browserContext.ts | 3 + packages/playwright-core/src/client/fetch.ts | 8 ++- tests/library/browsercontext-fetch.spec.ts | 5 ++ 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/examples/todomvc/tests/integration.spec.ts b/examples/todomvc/tests/integration.spec.ts index 007896bb2c..b438a13d15 100644 --- a/examples/todomvc/tests/integration.spec.ts +++ b/examples/todomvc/tests/integration.spec.ts @@ -54,6 +54,58 @@ test.describe('New Todo', () => { await checkNumberOfTodosInLocalStorage(page, 1); }); + test('should clear text input field when an item is added 3', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should clear text input field when an item is added 4', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should clear text input field when an item is added 5', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should clear text input field when an item is added 2', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + test('should append new items to the bottom of the list', async ({ page }) => { // Create 3 items. await createDefaultTodos(page); @@ -351,6 +403,22 @@ test.describe('Routing', () => { await expect(page.getByTestId('todo-item')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); }); + test('should allow me to display active items 2', async ({ page }) => { + await page.locator('.todo-list li .toggle').nth(1).check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(2); + await expect(page.getByTestId('todo-item')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should allow me to display active items 3', async ({ page }) => { + await page.locator('.todo-list li .toggle').nth(1).check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(2); + await expect(page.getByTestId('todo-item')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + test('should respect the back button', async ({ page }) => { await page.locator('.todo-list li .toggle').nth(1).check(); await checkNumberOfCompletedTodosInLocalStorage(page, 1); @@ -429,3 +497,4 @@ async function checkTodosInLocalStorage(page: Page, title: string) { return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); }, title); } + diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index f8f8977313..3f5640789e 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -445,6 +445,9 @@ export class BrowserContext extends ChannelOwner return; this._closeReason = options.reason; this._closeWasCalled = true; + await this._wrapApiCall(async () => { + await this.request.dispose(options); + }, true); await this._wrapApiCall(async () => { await this._browserType?._willCloseContext(this); for (const [harId, harParams] of this._harRecorders) { diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 34f0af3eae..2b32617f5b 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -103,7 +103,13 @@ export class APIRequestContext extends ChannelOwner { this._closeReason = options.reason; await this._instrumentation.onWillCloseRequestContext(this); - await this._channel.dispose(options); + try { + await this._channel.dispose(options); + } catch (e) { + if (isTargetClosedError(e)) + return; + throw e; + } this._tracing._resetStackCounter(); this._request?._contexts.delete(this); } diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index e4d7a6dbf3..f4088359cf 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -1255,3 +1255,8 @@ it('should not work after dispose', async ({ context, server }) => { await context.request.dispose(); expect(await context.request.get(server.EMPTY_PAGE).catch(e => e.message)).toContain(kTargetClosedErrorMessage); }); + +it('should not work after context dispose', async ({ context, server }) => { + await context.close({ reason: 'Test ended.' }); + expect(await context.request.get(server.EMPTY_PAGE).catch(e => e.message)).toContain('Test ended.'); +});