From 48c7fb6b0624a771bfcc7a8536ec4d18d780f455 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 13 Sep 2024 13:21:02 +0200 Subject: [PATCH 01/12] feat(library): accept `FormData` in `fetch` (#32602) Closes https://github.com/microsoft/playwright/issues/26520 by accepting `FormData`, which became stable in Node.js in v21. --- docs/src/api/class-apirequestcontext.md | 35 ++++++++++++++++---- docs/src/api/params.md | 12 +++++-- packages/playwright-core/src/client/fetch.ts | 15 +++++++-- packages/playwright-core/types/types.d.ts | 14 ++++---- tests/library/browsercontext-fetch.spec.ts | 16 +++++++++ 5 files changed, 73 insertions(+), 19 deletions(-) diff --git a/docs/src/api/class-apirequestcontext.md b/docs/src/api/class-apirequestcontext.md index 1d3e728235..f56087d1bd 100644 --- a/docs/src/api/class-apirequestcontext.md +++ b/docs/src/api/class-apirequestcontext.md @@ -159,7 +159,10 @@ context cookies from the response. The method will automatically follow redirect ### option: APIRequestContext.delete.data = %%-js-python-csharp-fetch-option-data-%% * since: v1.17 -### option: APIRequestContext.delete.form = %%-js-python-fetch-option-form-%% +### option: APIRequestContext.delete.form = %%-js-fetch-option-form-%% +* since: v1.17 + +### option: APIRequestContext.delete.form = %%-python-fetch-option-form-%% * since: v1.17 ### option: APIRequestContext.delete.form = %%-csharp-fetch-option-form-%% @@ -332,7 +335,10 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/ ### option: APIRequestContext.fetch.data = %%-js-python-csharp-fetch-option-data-%% * since: v1.16 -### option: APIRequestContext.fetch.form = %%-js-python-fetch-option-form-%% +### option: APIRequestContext.fetch.form = %%-js-fetch-option-form-%% +* since: v1.16 + +### option: APIRequestContext.fetch.form = %%-python-fetch-option-form-%% * since: v1.16 ### option: APIRequestContext.fetch.form = %%-csharp-fetch-option-form-%% @@ -442,7 +448,10 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query ### option: APIRequestContext.get.data = %%-js-python-csharp-fetch-option-data-%% * since: v1.26 -### option: APIRequestContext.get.form = %%-js-python-fetch-option-form-%% +### option: APIRequestContext.get.form = %%-js-fetch-option-form-%% +* since: v1.26 + +### option: APIRequestContext.get.form = %%-python-fetch-option-form-%% * since: v1.26 ### option: APIRequestContext.get.form = %%-csharp-fetch-option-form-%% @@ -504,7 +513,10 @@ context cookies from the response. The method will automatically follow redirect ### option: APIRequestContext.head.data = %%-js-python-csharp-fetch-option-data-%% * since: v1.26 -### option: APIRequestContext.head.form = %%-js-python-fetch-option-form-%% +### option: APIRequestContext.head.form = %%-python-fetch-option-form-%% +* since: v1.26 + +### option: APIRequestContext.head.form = %%-js-fetch-option-form-%% * since: v1.26 ### option: APIRequestContext.head.form = %%-csharp-fetch-option-form-%% @@ -566,7 +578,10 @@ context cookies from the response. The method will automatically follow redirect ### option: APIRequestContext.patch.data = %%-js-python-csharp-fetch-option-data-%% * since: v1.16 -### option: APIRequestContext.patch.form = %%-js-python-fetch-option-form-%% +### option: APIRequestContext.patch.form = %%-js-fetch-option-form-%% +* since: v1.16 + +### option: APIRequestContext.patch.form = %%-python-fetch-option-form-%% * since: v1.16 ### option: APIRequestContext.patch.form = %%-csharp-fetch-option-form-%% @@ -749,7 +764,10 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar ### option: APIRequestContext.post.data = %%-js-python-csharp-fetch-option-data-%% * since: v1.16 -### option: APIRequestContext.post.form = %%-js-python-fetch-option-form-%% +### option: APIRequestContext.post.form = %%-js-fetch-option-form-%% +* since: v1.16 + +### option: APIRequestContext.post.form = %%-python-fetch-option-form-%% * since: v1.16 ### option: APIRequestContext.post.form = %%-csharp-fetch-option-form-%% @@ -811,7 +829,10 @@ context cookies from the response. The method will automatically follow redirect ### option: APIRequestContext.put.data = %%-js-python-csharp-fetch-option-data-%% * since: v1.16 -### option: APIRequestContext.put.form = %%-js-python-fetch-option-form-%% +### option: APIRequestContext.put.form = %%-python-fetch-option-form-%% +* since: v1.16 + +### option: APIRequestContext.put.form = %%-js-fetch-option-form-%% * since: v1.16 ### option: APIRequestContext.put.form = %%-csharp-fetch-option-form-%% diff --git a/docs/src/api/params.md b/docs/src/api/params.md index cbec1a5e25..608855ab2a 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -405,8 +405,16 @@ Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to d Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. -## js-python-fetch-option-form -* langs: js, python +## js-fetch-option-form +* langs: js +- `form` <[Object]<[string], [string]|[float]|[boolean]>|[FormData]> + +Provides an object that will be serialized as html form using `application/x-www-form-urlencoded` encoding and sent as +this request body. If this parameter is specified `content-type` header will be set to `application/x-www-form-urlencoded` +unless explicitly provided. + +## python-fetch-option-form +* langs: python - `form` <[Object]<[string], [string]|[float]|[boolean]>> Provides an object that will be serialized as html form using `application/x-www-form-urlencoded` encoding and sent as diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 87c31579b5..58928532ac 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -36,8 +36,8 @@ export type FetchOptions = { method?: string, headers?: Headers, data?: string | Buffer | Serializable, - form?: { [key: string]: string|number|boolean; }; - multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; }; + form?: { [key: string]: string|number|boolean; } | FormData; + multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; } | FormData; timeout?: number, failOnStatusCode?: boolean, ignoreHTTPSErrors?: boolean, @@ -202,7 +202,16 @@ export class APIRequestContext extends ChannelOwner Date: Fri, 13 Sep 2024 06:13:11 -0700 Subject: [PATCH 02/12] feat(webkit): roll to r2075 (#32610) 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 23974d6523..cf83b5413f 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2073", + "revision": "2075", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 9e99c86f003d4d8a701540489ba61d44ea8569ee Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 13 Sep 2024 16:13:23 +0200 Subject: [PATCH 03/12] chore: unhide merge-reports command (#32605) --- packages/playwright/src/program.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 1bf2fb42b2..dd8d181676 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -133,7 +133,7 @@ Examples: } function addMergeReportsCommand(program: Command) { - const command = program.command('merge-reports [dir]', { hidden: true }); + const command = program.command('merge-reports [dir]'); command.description('merge multiple blob reports (for sharded tests) into a single report'); command.action(async (dir, options) => { try { From 9bb1c86f93a2b369796edf1ce5ccf8f091f9697f Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 13 Sep 2024 17:24:38 +0200 Subject: [PATCH 04/12] feat(test runner): don't run tests on --watch start (#32583) Closes https://github.com/microsoft/playwright/issues/32580. --- .../src/isomorphic/testServerInterface.ts | 1 + packages/playwright/src/runner/testServer.ts | 4 +- packages/playwright/src/runner/watchMode.ts | 11 +-- tests/playwright-test/watch.spec.ts | 76 +++++++++---------- 4 files changed, 44 insertions(+), 48 deletions(-) diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 28f82688dc..22cb9e35ef 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -28,6 +28,7 @@ export interface TestServerInterface { closeOnDisconnect?: boolean, interceptStdio?: boolean, watchTestDirs?: boolean, + populateDependenciesOnList?: boolean, }): Promise; ping(params: {}): Promise; diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index f34a7314f1..5d67385dc5 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -79,6 +79,7 @@ export class TestServerDispatcher implements TestServerInterface { private _serializer = require.resolve('./uiModeReporter'); private _watchTestDirs = false; private _closeOnDisconnect = false; + private _populateDependenciesOnList = false; constructor(configLocation: ConfigLocation) { this._configLocation = configLocation; @@ -113,6 +114,7 @@ export class TestServerDispatcher implements TestServerInterface { this._closeOnDisconnect = !!params.closeOnDisconnect; await this._setInterceptStdio(!!params.interceptStdio); this._watchTestDirs = !!params.watchTestDirs; + this._populateDependenciesOnList = !!params.populateDependenciesOnList; } async ping() {} @@ -252,7 +254,7 @@ export class TestServerDispatcher implements TestServerInterface { config.cliListOnly = true; const status = await runTasks(new TestRun(config, reporter), [ - createLoadTask('out-of-process', { failOnLoadErrors: false, filterOnly: false }), + createLoadTask('out-of-process', { failOnLoadErrors: false, filterOnly: false, populateDependencies: this._populateDependenciesOnList }), createReportBeginTask(), ]); return { config, report, reporter, status }; diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index ba2c5a34e7..603f066601 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -122,7 +122,7 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp }); testServerConnection.onReport(report => teleSuiteUpdater.processTestReportEvent(report)); - await testServerConnection.initialize({ interceptStdio: false, watchTestDirs: true }); + await testServerConnection.initialize({ interceptStdio: false, watchTestDirs: true, populateDependenciesOnList: true }); await testServerConnection.runGlobalSetup({}); const { report } = await testServerConnection.listTests({}); @@ -133,9 +133,6 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: string[], dirtyTestIds?: string[] } = { type: 'regular' }; let result: FullResult['status'] = 'passed'; - // Enter the watch loop. - await runTests(options, testServerConnection); - while (true) { printPrompt(); const readCommandPromise = readCommand(); @@ -330,7 +327,7 @@ Change settings let showBrowserServer: PlaywrightServer | undefined; let connectWsEndpoint: string | undefined = undefined; -let seq = 0; +let seq = 1; function printConfiguration(options: WatchModeOptions, title?: string) { const packageManagerCommand = getPackageManagerExecCommand(); @@ -344,9 +341,7 @@ function printConfiguration(options: WatchModeOptions, title?: string) { tokens.push(...options.files.map(a => colors.bold(a))); if (title) tokens.push(colors.dim(`(${title})`)); - if (seq) - tokens.push(colors.dim(`#${seq}`)); - ++seq; + tokens.push(colors.dim(`#${seq++}`)); const lines: string[] = []; const sep = separator(); lines.push('\x1Bc' + sep); diff --git a/tests/playwright-test/watch.spec.ts b/tests/playwright-test/watch.spec.ts index f4d1fd72ce..ec05e5bb68 100644 --- a/tests/playwright-test/watch.spec.ts +++ b/tests/playwright-test/watch.spec.ts @@ -174,15 +174,16 @@ test('should print dependencies in mixed CJS/ESM mode 2', async ({ runInlineTest }); }); -test('should perform initial run', async ({ runWatchTest }) => { +test('should not perform initial run', async ({ runWatchTest }) => { const testProcess = await runWatchTest({ 'a.test.ts': ` import { test, expect } from '@playwright/test'; test('passes', () => {}); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); + + expect(testProcess.output).not.toContain('a.test.ts'); }); test('should quit on Q', async ({ runWatchTest }) => { @@ -206,7 +207,6 @@ test('should run tests on Enter', async ({ runWatchTest }) => { test('passes', () => {}); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); testProcess.write('\r\n'); @@ -222,7 +222,6 @@ test('should run tests on R', async ({ runWatchTest }) => { test('passes', () => {}); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); testProcess.write('r'); @@ -246,6 +245,10 @@ test('should run failed tests on F', async ({ runWatchTest }) => { test('fails', () => { expect(1).toBe(2); }); `, }); + await testProcess.waitForOutput('Waiting for file changes.'); + testProcess.write('\r\n'); + + await testProcess.waitForOutput('npx playwright test #1'); await testProcess.waitForOutput('a.test.ts:3:11 › passes'); await testProcess.waitForOutput('b.test.ts:3:11 › passes'); await testProcess.waitForOutput('c.test.ts:3:11 › fails'); @@ -253,7 +256,7 @@ test('should run failed tests on F', async ({ runWatchTest }) => { await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); testProcess.write('f'); - await testProcess.waitForOutput('npx playwright test (running failed tests) #1'); + await testProcess.waitForOutput('npx playwright test (running failed tests) #2'); await testProcess.waitForOutput('c.test.ts:3:11 › fails'); expect(testProcess.output).not.toContain('a.test.ts:3:11'); }); @@ -269,8 +272,6 @@ test('should respect file filter P', async ({ runWatchTest }) => { test('passes', () => {}); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › passes'); - await testProcess.waitForOutput('b.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); testProcess.write('p'); @@ -294,6 +295,11 @@ test('should respect project filter C', async ({ runWatchTest, writeFiles }) => `, }; const testProcess = await runWatchTest(files, { project: 'foo' }); + await testProcess.waitForOutput('Waiting for file changes.'); + testProcess.clearOutput(); + testProcess.write('\r\n'); + + await testProcess.waitForOutput('npx playwright test --project foo #1'); await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); @@ -303,7 +309,7 @@ test('should respect project filter C', async ({ runWatchTest, writeFiles }) => await testProcess.waitForOutput('bar'); testProcess.write(' '); testProcess.write('\r\n'); - await testProcess.waitForOutput('npx playwright test --project foo #1'); + await testProcess.waitForOutput('npx playwright test --project foo #2'); await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes'); expect(testProcess.output).not.toContain('[bar] › a.test.ts:3:11 › passes'); @@ -329,8 +335,6 @@ test('should respect file filter P and split files', async ({ runWatchTest }) => test('passes', () => {}); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › passes'); - await testProcess.waitForOutput('b.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); testProcess.write('p'); @@ -353,8 +357,6 @@ test('should respect title filter T', async ({ runWatchTest }) => { test('title 2', () => {}); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › title 1'); - await testProcess.waitForOutput('b.test.ts:3:11 › title 2'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); testProcess.write('t'); @@ -381,6 +383,11 @@ test('should re-run failed tests on F > R', async ({ runWatchTest }) => { test('fails', () => { expect(1).toBe(2); }); `, }); + await testProcess.waitForOutput('Waiting for file changes.'); + testProcess.clearOutput(); + testProcess.write('\r\n'); + + await testProcess.waitForOutput('npx playwright test #1'); await testProcess.waitForOutput('a.test.ts:3:11 › passes'); await testProcess.waitForOutput('b.test.ts:3:11 › passes'); await testProcess.waitForOutput('c.test.ts:3:11 › fails'); @@ -388,12 +395,12 @@ test('should re-run failed tests on F > R', async ({ runWatchTest }) => { await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); testProcess.write('f'); - await testProcess.waitForOutput('npx playwright test (running failed tests) #1'); + await testProcess.waitForOutput('npx playwright test (running failed tests) #2'); await testProcess.waitForOutput('c.test.ts:3:11 › fails'); expect(testProcess.output).not.toContain('a.test.ts:3:11'); testProcess.clearOutput(); testProcess.write('r'); - await testProcess.waitForOutput('npx playwright test (re-running tests) #2'); + await testProcess.waitForOutput('npx playwright test (re-running tests) #3'); await testProcess.waitForOutput('c.test.ts:3:11 › fails'); expect(testProcess.output).not.toContain('a.test.ts:3:11'); }); @@ -413,10 +420,6 @@ test('should run on changed files', async ({ runWatchTest, writeFiles }) => { test('fails', () => { expect(1).toBe(2); }); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › passes'); - await testProcess.waitForOutput('b.test.ts:3:11 › passes'); - await testProcess.waitForOutput('c.test.ts:3:11 › fails'); - await testProcess.waitForOutput('Error: expect(received).toBe(expected)'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); await writeFiles({ @@ -457,9 +460,6 @@ test('should run on changed deps', async ({ runWatchTest, writeFiles }) => { console.log('old helper'); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › passes'); - await testProcess.waitForOutput('b.test.ts:4:11 › passes'); - await testProcess.waitForOutput('old helper'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); await writeFiles({ @@ -490,9 +490,6 @@ test('should run on changed deps in ESM', async ({ runWatchTest, writeFiles }) = console.log('old helper'); `, }); - await testProcess.waitForOutput('a.test.ts:3:7 › passes'); - await testProcess.waitForOutput('b.test.ts:4:7 › passes'); - await testProcess.waitForOutput('old helper'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); await writeFiles({ @@ -521,10 +518,6 @@ test('should re-run changed files on R', async ({ runWatchTest, writeFiles }) => test('fails', () => { expect(1).toBe(2); }); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › passes'); - await testProcess.waitForOutput('b.test.ts:3:11 › passes'); - await testProcess.waitForOutput('c.test.ts:3:11 › fails'); - await testProcess.waitForOutput('Error: expect(received).toBe(expected)'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); await writeFiles({ @@ -556,8 +549,6 @@ test('should not trigger on changes to non-tests', async ({ runWatchTest, writeF test('passes', () => {}); `, }); - await testProcess.waitForOutput('a.test.ts:3:11 › passes'); - await testProcess.waitForOutput('b.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); @@ -582,6 +573,10 @@ test('should only watch selected projects', async ({ runWatchTest, writeFiles }) test('passes', () => {}); `, }, undefined, undefined, { additionalArgs: ['--project=foo'] }); + await testProcess.waitForOutput('Waiting for file changes.'); + testProcess.clearOutput(); + testProcess.write('\r\n'); + await testProcess.waitForOutput('npx playwright test --project foo'); await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes'); expect(testProcess.output).not.toContain('[bar]'); @@ -612,6 +607,10 @@ test('should watch filtered files', async ({ runWatchTest, writeFiles }) => { test('passes', () => {}); `, }, undefined, undefined, { additionalArgs: ['a.test.ts'] }); + await testProcess.waitForOutput('Waiting for file changes.'); + testProcess.clearOutput(); + testProcess.write('\r\n'); + await testProcess.waitForOutput('npx playwright test a.test.ts'); await testProcess.waitForOutput('a.test.ts:3:11 › passes'); expect(testProcess.output).not.toContain('b.test'); @@ -640,6 +639,10 @@ test('should not watch unfiltered files', async ({ runWatchTest, writeFiles }) = test('passes', () => {}); `, }, undefined, undefined, { additionalArgs: ['a.test.ts'] }); + await testProcess.waitForOutput('Waiting for file changes.'); + testProcess.clearOutput(); + testProcess.write('\r\n'); + await testProcess.waitForOutput('npx playwright test a.test.ts'); await testProcess.waitForOutput('a.test.ts:3:11 › passes'); expect(testProcess.output).not.toContain('b.test'); @@ -684,10 +687,7 @@ test('should run CT on changed deps', async ({ runWatchTest, writeFiles }) => { }); `, }); - await testProcess.waitForOutput('button.spec.tsx:4:11 › pass'); - await testProcess.waitForOutput('link.spec.tsx:3:11 › pass'); await testProcess.waitForOutput('Waiting for file changes.'); - testProcess.clearOutput(); await writeFiles({ 'src/button.tsx': ` export const Button = () => ; @@ -732,10 +732,7 @@ test('should run CT on indirect deps change', async ({ runWatchTest, writeFiles }); `, }); - await testProcess.waitForOutput('button.spec.tsx:4:11 › pass'); - await testProcess.waitForOutput('link.spec.tsx:3:11 › pass'); await testProcess.waitForOutput('Waiting for file changes.'); - testProcess.clearOutput(); await writeFiles({ 'src/button.css': ` button { color: blue; } @@ -776,10 +773,7 @@ test('should run CT on indirect deps change ESM mode', async ({ runWatchTest, wr }); `, }); - await testProcess.waitForOutput('button.spec.tsx:4:7 › pass'); - await testProcess.waitForOutput('link.spec.tsx:3:7 › pass'); await testProcess.waitForOutput('Waiting for file changes.'); - testProcess.clearOutput(); await writeFiles({ 'src/button.css': ` button { color: blue; } @@ -809,6 +803,10 @@ test('should run global teardown before exiting', async ({ runWatchTest }) => { }); `, }); + await testProcess.waitForOutput('Waiting for file changes.'); + testProcess.clearOutput(); + testProcess.write('\r\n'); + await testProcess.waitForOutput('a.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.write('\x1B'); From 79cba7d70455132f0069a8cf1ef099f356b2eb41 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 13 Sep 2024 17:34:34 +0200 Subject: [PATCH 05/12] chore: introduce option overrides on context/browser (#32606) --- .../src/server/android/android.ts | 3 ++- .../src/server/bidi/bidiBrowser.ts | 4 ++-- .../src/server/bidi/bidiChromium.ts | 3 ++- .../playwright-core/src/server/browser.ts | 10 ++++++--- .../src/server/browserContext.ts | 15 ++++++------- .../playwright-core/src/server/browserType.ts | 21 +++++++++---------- .../src/server/chromium/chromium.ts | 6 +++--- .../src/server/chromium/crBrowser.ts | 13 ++++++------ .../src/server/chromium/crPage.ts | 4 ++-- .../src/server/electron/electron.ts | 3 ++- packages/playwright-core/src/server/fetch.ts | 2 +- .../src/server/firefox/ffBrowser.ts | 16 +++++++------- .../socksClientCertificatesInterceptor.ts | 16 +++++++------- packages/playwright-core/src/server/types.ts | 10 ++++++++- .../src/server/webkit/webkit.ts | 3 ++- .../src/server/webkit/wkBrowser.ts | 15 ++++++------- tests/library/client-certificates.spec.ts | 1 + 17 files changed, 81 insertions(+), 64 deletions(-) diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 0b4cb331b0..1af083916c 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -29,6 +29,7 @@ import { validateBrowserContextOptions } from '../browserContext'; import { ProgressController } from '../progress'; import { CRBrowser } from '../chromium/crBrowser'; import { helper } from '../helper'; +import type * as types from '../types'; import { PipeTransport } from '../../protocol/transport'; import { RecentLogsCollector } from '../../utils/debugLogger'; import { gracefullyCloseSet } from '../../utils/processLauncher'; @@ -309,7 +310,7 @@ export class AndroidDevice extends SdkObject { return await this._connectToBrowser(socketName); } - private async _connectToBrowser(socketName: string, options: channels.BrowserNewContextParams = {}): Promise { + private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions = {}): Promise { const socket = await this._waitForLocalAbstract(socketName); const androidBrowser = new AndroidBrowser(this, socket); await androidBrowser._init(); diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index 0c658a82b4..cc98e2ff3a 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -111,7 +111,7 @@ export class BidiBrowser extends Browser { this._didClose(); } - async doCreateNewContext(options: channels.BrowserNewContextParams): Promise { + async doCreateNewContext(options: types.BrowserContextOptions): Promise { const { userContext } = await this._browserSession.send('browser.createUserContext', {}); const context = new BidiBrowserContext(this, userContext, options); await context._initialize(); @@ -190,7 +190,7 @@ export class BidiBrowser extends Browser { export class BidiBrowserContext extends BrowserContext { declare readonly _browser: BidiBrowser; - constructor(browser: BidiBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) { + constructor(browser: BidiBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) { super(browser, options, browserContextId); this._authenticateProxyViaHeader(); } diff --git a/packages/playwright-core/src/server/bidi/bidiChromium.ts b/packages/playwright-core/src/server/bidi/bidiChromium.ts index e94dabf072..32751bd51a 100644 --- a/packages/playwright-core/src/server/bidi/bidiChromium.ts +++ b/packages/playwright-core/src/server/bidi/bidiChromium.ts @@ -91,7 +91,7 @@ export class BidiChromium extends BrowserType { } private _innerDefaultArgs(options: types.LaunchOptions): string[] { - const { args = [], proxy } = options; + const { args = [] } = options; const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); if (userDataDirArg) throw this._createUserDataDirArgMisuseError('--user-data-dir'); @@ -125,6 +125,7 @@ export class BidiChromium extends BrowserType { } if (options.chromiumSandbox !== true) chromeArguments.push('--no-sandbox'); + const proxy = options.proxyOverride || options.proxy; if (proxy) { const proxyURL = new URL(proxy.server); const isSocks = proxyURL.protocol === 'socks5:'; diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index 663b9be377..04a23e7eac 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -41,7 +41,7 @@ export type BrowserOptions = { downloadsPath: string, tracesDir: string, headful?: boolean, - persistent?: channels.BrowserNewContextParams, // Undefined means no persistent context. + persistent?: types.BrowserContextOptions, // Undefined means no persistent context. browserProcess: BrowserProcess, customExecutablePath?: string; proxy?: ProxySettings, @@ -74,15 +74,19 @@ export abstract class Browser extends SdkObject { this.instrumentation.onBrowserOpen(this); } - abstract doCreateNewContext(options: channels.BrowserNewContextParams): Promise; + abstract doCreateNewContext(options: types.BrowserContextOptions): Promise; abstract contexts(): BrowserContext[]; abstract isConnected(): boolean; abstract version(): string; abstract userAgent(): string; - async newContext(metadata: CallMetadata, options: channels.BrowserNewContextParams): Promise { + async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise { validateBrowserContextOptions(options, this.options); const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options); + if (clientCertificatesProxy) { + options.proxyOverride = await clientCertificatesProxy.listen(); + options.internalIgnoreHTTPSErrors = true; + } let context; try { context = await this.doCreateNewContext(options); diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 8ddbe68f89..e1166f3e97 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -68,7 +68,7 @@ export abstract class BrowserContext extends SdkObject { readonly _timeoutSettings = new TimeoutSettings(); readonly _pageBindings = new Map(); readonly _activeProgressControllers = new Set(); - readonly _options: channels.BrowserNewContextParams; + readonly _options: types.BrowserContextOptions; _requestInterceptor?: network.RouteHandler; private _isPersistentContext: boolean; private _closedStatus: 'open' | 'closing' | 'closed' = 'open'; @@ -93,7 +93,7 @@ export abstract class BrowserContext extends SdkObject { readonly clock: Clock; _clientCertificatesProxy: ClientCertificatesProxy | undefined; - constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) { + constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { super(browser, 'browser-context'); this.attribution.context = this; this._browser = browser; @@ -659,19 +659,16 @@ export function assertBrowserContextIsNotOwned(context: BrowserContext) { } } -export async function createClientCertificatesProxyIfNeeded(options: channels.BrowserNewContextOptions, browserOptions?: BrowserOptions) { +export async function createClientCertificatesProxyIfNeeded(options: types.BrowserContextOptions, browserOptions?: BrowserOptions) { if (!options.clientCertificates?.length) return; if ((options.proxy?.server && options.proxy?.server !== 'per-context') || (browserOptions?.proxy?.server && browserOptions?.proxy?.server !== 'http://per-context')) throw new Error('Cannot specify both proxy and clientCertificates'); verifyClientCertificates(options.clientCertificates); - const clientCertificatesProxy = new ClientCertificatesProxy(options); - options.proxy = { server: await clientCertificatesProxy.listen() }; - options.ignoreHTTPSErrors = true; - return clientCertificatesProxy; + return new ClientCertificatesProxy(options); } -export function validateBrowserContextOptions(options: channels.BrowserNewContextParams, browserOptions: BrowserOptions) { +export function validateBrowserContextOptions(options: types.BrowserContextOptions, browserOptions: BrowserOptions) { if (options.noDefaultViewport && options.deviceScaleFactor !== undefined) throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`); if (options.noDefaultViewport && !!options.isMobile) @@ -720,7 +717,7 @@ export function verifyGeolocation(geolocation?: types.Geolocation) { throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`); } -export function verifyClientCertificates(clientCertificates?: channels.BrowserNewContextParams['clientCertificates']) { +export function verifyClientCertificates(clientCertificates?: types.BrowserContextOptions['clientCertificates']) { if (!clientCertificates) return; for (const cert of clientCertificates) { diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index d0c3174a59..286ec27c29 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -92,27 +92,26 @@ export abstract class BrowserType extends SdkObject { return browser; } - async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise { - options = this._validateLaunchOptions(options); + async launchPersistentContext(metadata: CallMetadata, userDataDir: string, persistentContextOptions: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise { + const launchOptions = this._validateLaunchOptions(persistentContextOptions); if (this._useBidi) - options.useWebSocket = true; + launchOptions.useWebSocket = true; const controller = new ProgressController(metadata, this); - const persistent: channels.BrowserNewContextParams = { ...options }; controller.setLogName('browser'); const browser = await controller.run(async progress => { // Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors. - const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(persistent); + const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(persistentContextOptions); if (clientCertificatesProxy) - options.proxy = persistent.proxy; + launchOptions.proxyOverride = await clientCertificatesProxy?.listen(); progress.cleanupWhenAborted(() => clientCertificatesProxy?.close()); - const browser = await this._innerLaunchWithRetries(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); + const browser = await this._innerLaunchWithRetries(progress, launchOptions, persistentContextOptions, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy; return browser; - }, TimeoutSettings.launchTimeout(options)); + }, TimeoutSettings.launchTimeout(launchOptions)); return browser._defaultContext!; } - async _innerLaunchWithRetries(progress: Progress, options: types.LaunchOptions, persistent: channels.BrowserNewContextParams | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise { + async _innerLaunchWithRetries(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise { try { return await this._innerLaunch(progress, options, persistent, protocolLogger, userDataDir); } catch (error) { @@ -126,7 +125,7 @@ export abstract class BrowserType extends SdkObject { } } - async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: channels.BrowserNewContextParams | undefined, protocolLogger: types.ProtocolLogger, maybeUserDataDir?: string): Promise { + async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, maybeUserDataDir?: string): Promise { options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined; const browserLogsCollector = new RecentLogsCollector(); const { browserProcess, userDataDir, artifactsDir, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir); @@ -289,7 +288,7 @@ export abstract class BrowserType extends SdkObject { throw new Error('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium'); } - private _validateLaunchOptions(options: Options): Options { + private _validateLaunchOptions(options: types.LaunchOptions): types.LaunchOptions { const { devtools = false } = options; let { headless = !devtools, downloadsPath, proxy } = options; if (debugMode()) diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index d84ae3952e..30bd2871b8 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -31,7 +31,6 @@ import { CRDevTools } from './crDevTools'; import type { BrowserOptions, BrowserProcess } from '../browser'; import { Browser } from '../browser'; import type * as types from '../types'; -import type * as channels from '@protocol/channels'; import type { HTTPRequestParams } from '../../utils/network'; import { fetchData } from '../../utils/network'; import { getUserAgent } from '../../utils/userAgent'; @@ -98,7 +97,7 @@ export class Chromium extends BrowserType { await cleanedUp; }; const browserProcess: BrowserProcess = { close: doClose, kill: doClose }; - const persistent: channels.BrowserNewContextParams = { noDefaultViewport: true }; + const persistent: types.BrowserContextOptions = { noDefaultViewport: true }; const browserOptions: BrowserOptions = { slowMo: options.slowMo, name: 'chromium', @@ -287,7 +286,7 @@ export class Chromium extends BrowserType { } private _innerDefaultArgs(options: types.LaunchOptions): string[] { - const { args = [], proxy } = options; + const { args = [] } = options; const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); if (userDataDirArg) throw this._createUserDataDirArgMisuseError('--user-data-dir'); @@ -321,6 +320,7 @@ export class Chromium extends BrowserType { } if (options.chromiumSandbox !== true) chromeArguments.push('--no-sandbox'); + const proxy = options.proxyOverride || options.proxy; if (proxy) { const proxyURL = new URL(proxy.server); const isSocks = proxyURL.protocol === 'socks5:'; diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 42b916c186..e0409c1b16 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -100,18 +100,19 @@ export class CRBrowser extends Browser { this._session.on('Browser.downloadProgress', this._onDownloadProgress.bind(this)); } - async doCreateNewContext(options: channels.BrowserNewContextParams): Promise { + async doCreateNewContext(options: types.BrowserContextOptions): Promise { + const proxy = options.proxyOverride || options.proxy; let proxyBypassList = undefined; - if (options.proxy) { + if (proxy) { if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK) - proxyBypassList = options.proxy.bypass; + proxyBypassList = proxy.bypass; else - proxyBypassList = '<-loopback>' + (options.proxy.bypass ? `,${options.proxy.bypass}` : ''); + proxyBypassList = '<-loopback>' + (proxy.bypass ? `,${proxy.bypass}` : ''); } const { browserContextId } = await this._session.send('Target.createBrowserContext', { disposeOnDetach: true, - proxyServer: options.proxy ? options.proxy.server : undefined, + proxyServer: proxy ? proxy.server : undefined, proxyBypassList, }); const context = new CRBrowserContext(this, browserContextId, options); @@ -340,7 +341,7 @@ export class CRBrowserContext extends BrowserContext { declare readonly _browser: CRBrowser; - constructor(browser: CRBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) { + constructor(browser: CRBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) { super(browser, options, browserContextId); this._authenticateProxyViaCredentials(); } diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 002dfa09be..5a7fb5e4af 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -543,7 +543,7 @@ class FrameSession { const options = this._crPage._browserContext._options; if (options.bypassCSP) promises.push(this._client.send('Page.setBypassCSP', { enabled: true })); - if (options.ignoreHTTPSErrors) + if (options.ignoreHTTPSErrors || options.internalIgnoreHTTPSErrors) promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true })); if (this._isMainFrame()) promises.push(this._updateViewport()); @@ -1213,7 +1213,7 @@ async function emulateTimezone(session: CRSession, timezoneId: string) { const contextDelegateSymbol = Symbol('delegate'); // Chromium reference: https://source.chromium.org/chromium/chromium/src/+/main:components/embedder_support/user_agent_utils.cc;l=434;drc=70a6711e08e9f9e0d8e4c48e9ba5cab62eb010c2 -function calculateUserAgentMetadata(options: channels.BrowserNewContextParams) { +function calculateUserAgentMetadata(options: types.BrowserContextOptions) { const ua = options.userAgent; if (!ua) return undefined; diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index b8f361b48a..1606c407d5 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -36,6 +36,7 @@ import type { BrowserWindow } from 'electron'; import type { Progress } from '../progress'; import { ProgressController } from '../progress'; import { helper } from '../helper'; +import type * as types from '../types'; import { eventsHelper } from '../../utils/eventsHelper'; import type { BrowserOptions, BrowserProcess } from '../browser'; import type { Playwright } from '../playwright'; @@ -265,7 +266,7 @@ export class Electron extends SdkObject { close: gracefullyClose, kill }; - const contextOptions: channels.BrowserNewContextParams = { + const contextOptions: types.BrowserContextOptions = { ...options, noDefaultViewport: true, }; diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 7a6102a50d..5d00dc05a6 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -50,7 +50,7 @@ type FetchRequestOptions = { timeoutSettings: TimeoutSettings; ignoreHTTPSErrors?: boolean; baseURL?: string; - clientCertificates?: channels.BrowserNewContextOptions['clientCertificates']; + clientCertificates?: types.BrowserContextOptions['clientCertificates']; }; type HeadersObject = Readonly<{ [name: string]: string }>; diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 94b90bbcea..b26a2850ee 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -58,8 +58,9 @@ export class FFBrowser extends Browser { browser._defaultContext = new FFBrowserContext(browser, undefined, options.persistent); promises.push((browser._defaultContext as FFBrowserContext)._initialize()); } - if (options.proxy) - promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(options.proxy))); + const proxy = options.originalLaunchOptions.proxyOverride || options.proxy; + if (proxy) + promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(proxy))); await Promise.all(promises); return browser; } @@ -88,7 +89,7 @@ export class FFBrowser extends Browser { return !this._connection._closed; } - async doCreateNewContext(options: channels.BrowserNewContextParams): Promise { + async doCreateNewContext(options: types.BrowserContextOptions): Promise { if (options.isMobile) throw new Error('options.isMobile is not supported in Firefox'); const { browserContextId } = await this.session.send('Browser.createBrowserContext', { removeOnDetach: true }); @@ -172,7 +173,7 @@ export class FFBrowser extends Browser { export class FFBrowserContext extends BrowserContext { declare readonly _browser: FFBrowser; - constructor(browser: FFBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) { + constructor(browser: FFBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) { super(browser, options, browserContextId); } @@ -205,7 +206,7 @@ export class FFBrowserContext extends BrowserContext { promises.push(this._browser.session.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent })); if (this._options.bypassCSP) promises.push(this._browser.session.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true })); - if (this._options.ignoreHTTPSErrors) + if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors) promises.push(this._browser.session.send('Browser.setIgnoreHTTPSErrors', { browserContextId, ignoreHTTPSErrors: true })); if (this._options.javaScriptEnabled === false) promises.push(this._browser.session.send('Browser.setJavaScriptDisabled', { browserContextId, javaScriptDisabled: true })); @@ -251,10 +252,11 @@ export class FFBrowserContext extends BrowserContext { }); })); } - if (this._options.proxy) { + const proxy = this._options.proxyOverride || this._options.proxy; + if (proxy) { promises.push(this._browser.session.send('Browser.setContextProxy', { browserContextId: this._browserContextId, - ...toJugglerProxyOptions(this._options.proxy) + ...toJugglerProxyOptions(proxy) })); } diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 2dd900bf89..6d4b334dba 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -23,7 +23,7 @@ import { createSocket, createTLSSocket } from '../utils/happy-eyeballs'; import { escapeHTML, generateSelfSignedCertificate, ManualPromise, rewriteErrorMessage } from '../utils'; import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy'; import { SocksProxy } from '../common/socksProxy'; -import type * as channels from '@protocol/channels'; +import type * as types from './types'; import { debugLogger } from '../utils/debugLogger'; let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined; @@ -235,7 +235,7 @@ export class ClientCertificatesProxy { alpnCache: ALPNCache; constructor( - contextOptions: Pick + contextOptions: Pick ) { this.alpnCache = new ALPNCache(); this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors; @@ -261,9 +261,9 @@ export class ClientCertificatesProxy { loadDummyServerCertsIfNeeded(); } - _initSecureContexts(clientCertificates: channels.BrowserNewContextOptions['clientCertificates']) { + _initSecureContexts(clientCertificates: types.BrowserContextOptions['clientCertificates']) { // Step 1. Group certificates by origin. - const origin2certs = new Map(); + const origin2certs = new Map(); for (const cert of clientCertificates || []) { const origin = normalizeOrigin(cert.origin); const certs = origin2certs.get(origin) || []; @@ -282,9 +282,9 @@ export class ClientCertificatesProxy { } } - public async listen(): Promise { + public async listen() { const port = await this._socksProxy.listen(0, '127.0.0.1'); - return `socks5://127.0.0.1:${port}`; + return { server: `socks5://127.0.0.1:${port}` }; } public async close() { @@ -301,7 +301,7 @@ function normalizeOrigin(origin: string): string { } function convertClientCertificatesToTLSOptions( - clientCertificates: channels.BrowserNewContextOptions['clientCertificates'] + clientCertificates: types.BrowserContextOptions['clientCertificates'] ): Pick | undefined { if (!clientCertificates || !clientCertificates.length) return; @@ -322,7 +322,7 @@ function convertClientCertificatesToTLSOptions( } export function getMatchingTLSOptionsForOrigin( - clientCertificates: channels.BrowserNewContextOptions['clientCertificates'], + clientCertificates: types.BrowserContextOptions['clientCertificates'], origin: string ): Pick | undefined { const matchingCerts = clientCertificates?.filter(c => diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index 226216c397..b58ea5af83 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -150,7 +150,15 @@ export type NormalizedContinueOverrides = { export type EmulatedSize = { viewport: Size, screen: Size }; -export type LaunchOptions = channels.BrowserTypeLaunchOptions & { useWebSocket?: boolean }; +export type LaunchOptions = channels.BrowserTypeLaunchOptions & { + useWebSocket?: boolean, + proxyOverride?: ProxySettings, +}; + +export type BrowserContextOptions = channels.BrowserNewContextOptions & { + proxyOverride?: ProxySettings; + internalIgnoreHTTPSErrors?: boolean; +}; export type ProtocolLogger = (direction: 'send' | 'receive', message: object) => void; diff --git a/packages/playwright-core/src/server/webkit/webkit.ts b/packages/playwright-core/src/server/webkit/webkit.ts index b25b62a421..9a11f6c56d 100644 --- a/packages/playwright-core/src/server/webkit/webkit.ts +++ b/packages/playwright-core/src/server/webkit/webkit.ts @@ -53,7 +53,7 @@ export class WebKit extends BrowserType { } override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] { - const { args = [], proxy, headless } = options; + const { args = [], headless } = options; const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); if (userDataDirArg) throw this._createUserDataDirArgMisuseError('--user-data-dir'); @@ -68,6 +68,7 @@ export class WebKit extends BrowserType { webkitArguments.push(`--user-data-dir=${userDataDir}`); else webkitArguments.push(`--no-startup-window`); + const proxy = options.proxyOverride || options.proxy; if (proxy) { if (process.platform === 'darwin') { webkitArguments.push(`--proxy=${proxy.server}`); diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index 92231edd75..c9bda10ddd 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -81,12 +81,13 @@ export class WKBrowser extends Browser { this._didClose(); } - async doCreateNewContext(options: channels.BrowserNewContextParams): Promise { - const createOptions = options.proxy ? { - // Enable socks5 hostname resolution on Windows. Workaround can be removed once fixed upstream. + async doCreateNewContext(options: types.BrowserContextOptions): Promise { + const proxy = options.proxyOverride || options.proxy; + const createOptions = proxy ? { + // Enable socks5 hostname resolution on Windows. // See https://github.com/microsoft/playwright/issues/20451 - proxyServer: process.platform === 'win32' ? options.proxy.server.replace(/^socks5:\/\//, 'socks5h://') : options.proxy.server, - proxyBypassList: options.proxy.bypass + proxyServer: process.platform === 'win32' ? proxy.server.replace(/^socks5:\/\//, 'socks5h://') : proxy.server, + proxyBypassList: proxy.bypass } : undefined; const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions); options.userAgent = options.userAgent || DEFAULT_USER_AGENT; @@ -206,7 +207,7 @@ export class WKBrowser extends Browser { export class WKBrowserContext extends BrowserContext { declare readonly _browser: WKBrowser; - constructor(browser: WKBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) { + constructor(browser: WKBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) { super(browser, options, browserContextId); this._validateEmulatedViewport(options.viewport); this._authenticateProxyViaHeader(); @@ -221,7 +222,7 @@ export class WKBrowserContext extends BrowserContext { downloadPath: this._browser.options.downloadsPath, browserContextId })); - if (this._options.ignoreHTTPSErrors) + if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors) promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true })); if (this._options.locale) promises.push(this._browser._browserSession.send('Playwright.setLanguages', { browserContextId, languages: [this._options.locale] })); diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index 682df0b00f..899b9819be 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -546,6 +546,7 @@ test.describe('browser', () => { keyPath: asset('client-certificates/client/trusted/key.pem'), }; const page = await browser.newPage({ + ignoreHTTPSErrors: true, clientCertificates: [{ origin: new URL(serverURL).origin, ...baseOptions, From b82100adf5490bbbef5a55b0fa9658947a47bd55 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:12:35 -0700 Subject: [PATCH 06/12] feat(firefox-beta): roll to r1464 (#32615) 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 cf83b5413f..6e8da091b4 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -21,7 +21,7 @@ }, { "name": "firefox-beta", - "revision": "1463", + "revision": "1464", "installByDefault": false, "browserVersion": "131.0b2" }, From 5b28d2a84c0fa5232ab029ff1e6c7c73c10f9e42 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:12:43 -0700 Subject: [PATCH 07/12] feat(firefox): roll to r1464 (#32614) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- packages/playwright-core/src/server/firefox/protocol.d.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 6e8da091b4..de51a1643c 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,7 +15,7 @@ }, { "name": "firefox", - "revision": "1463", + "revision": "1464", "installByDefault": true, "browserVersion": "130.0" }, diff --git a/packages/playwright-core/src/server/firefox/protocol.d.ts b/packages/playwright-core/src/server/firefox/protocol.d.ts index 1da6d70122..f4a44d9d4d 100644 --- a/packages/playwright-core/src/server/firefox/protocol.d.ts +++ b/packages/playwright-core/src/server/firefox/protocol.d.ts @@ -315,6 +315,11 @@ export module Protocol { }; export type cancelDownloadReturnValue = void; } + export module Heap { + export type collectGarbageParameters = { + }; + export type collectGarbageReturnValue = void; + } export module Page { export type DOMPoint = { x: number; @@ -1124,6 +1129,7 @@ export module Protocol { "Browser.setForcedColors": Browser.setForcedColorsParameters; "Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsParameters; "Browser.cancelDownload": Browser.cancelDownloadParameters; + "Heap.collectGarbage": Heap.collectGarbageParameters; "Page.close": Page.closeParameters; "Page.setFileInputFiles": Page.setFileInputFilesParameters; "Page.addBinding": Page.addBindingParameters; @@ -1204,6 +1210,7 @@ export module Protocol { "Browser.setForcedColors": Browser.setForcedColorsReturnValue; "Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsReturnValue; "Browser.cancelDownload": Browser.cancelDownloadReturnValue; + "Heap.collectGarbage": Heap.collectGarbageReturnValue; "Page.close": Page.closeReturnValue; "Page.setFileInputFiles": Page.setFileInputFilesReturnValue; "Page.addBinding": Page.addBindingReturnValue; From f2a974b0451db21295a1dde2f8670483d8a5697b Mon Sep 17 00:00:00 2001 From: Matthew Jee Date: Fri, 13 Sep 2024 14:09:36 -0700 Subject: [PATCH 08/12] feat(api): add method to force garbage collection (#32383) --- .../firefox/juggler/protocol/PageHandler.js | 7 +++++ .../firefox/juggler/protocol/Protocol.js | 13 ++++++++- docs/src/api/class-page.md | 5 ++++ packages/playwright-core/src/client/page.ts | 4 +++ .../playwright-core/src/protocol/validator.ts | 2 ++ .../src/server/bidi/bidiPage.ts | 4 +++ .../src/server/chromium/crPage.ts | 4 +++ .../src/server/dispatchers/pageDispatcher.ts | 4 +++ .../src/server/firefox/ffPage.ts | 4 +++ packages/playwright-core/src/server/page.ts | 5 ++++ .../src/server/webkit/wkPage.ts | 4 +++ packages/playwright-core/types/types.d.ts | 5 ++++ packages/protocol/src/channels.ts | 4 +++ packages/protocol/src/protocol.yml | 2 ++ tests/page/page-force-gc.spec.ts | 27 +++++++++++++++++++ 15 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tests/page/page-force-gc.spec.ts diff --git a/browser_patches/firefox/juggler/protocol/PageHandler.js b/browser_patches/firefox/juggler/protocol/PageHandler.js index 8fa9a06361..bab151b392 100644 --- a/browser_patches/firefox/juggler/protocol/PageHandler.js +++ b/browser_patches/firefox/juggler/protocol/PageHandler.js @@ -256,6 +256,13 @@ class PageHandler { return await this._contentPage.send('disposeObject', options); } + async ['Heap.collectGarbage']() { + Services.obs.notifyObservers(null, "child-gc-request"); + Cu.forceGC(); + Services.obs.notifyObservers(null, "child-cc-request"); + Cu.forceCC(); + } + async ['Network.getResponseBody']({requestId}) { return this._pageNetwork.getResponseBody(requestId); } diff --git a/browser_patches/firefox/juggler/protocol/Protocol.js b/browser_patches/firefox/juggler/protocol/Protocol.js index 6c9b700f05..2b7ad56d6a 100644 --- a/browser_patches/firefox/juggler/protocol/Protocol.js +++ b/browser_patches/firefox/juggler/protocol/Protocol.js @@ -487,6 +487,17 @@ const Browser = { }, }; +const Heap = { + targets: ['page'], + types: {}, + events: {}, + methods: { + 'collectGarbage': { + params: {}, + }, + }, +}; + const Network = { targets: ['page'], types: networkTypes, @@ -1002,7 +1013,7 @@ const Accessibility = { } this.protocol = { - domains: {Browser, Page, Runtime, Network, Accessibility}, + domains: {Browser, Heap, Page, Runtime, Network, Accessibility}, }; this.checkScheme = checkScheme; this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme']; diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index e1aa908041..d36a96ee72 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2333,6 +2333,11 @@ last redirect. If cannot go forward, returns `null`. Navigate to the next page in history. +## async method: Page.forceGarbageCollection +* since: v1.47 + +Force the browser to perform garbage collection. + ### option: Page.goForward.waitUntil = %%-navigation-wait-until-%% * since: v1.8 diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index a10286fa9a..0bbe78f4c8 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -468,6 +468,10 @@ export class Page extends ChannelOwner implements api.Page return Response.fromNullable((await this._channel.goForward({ ...options, waitUntil })).response); } + async forceGarbageCollection() { + await this._channel.forceGarbageCollection(); + } + async emulateMedia(options: { media?: 'screen' | 'print' | null, colorScheme?: 'dark' | 'light' | 'no-preference' | null, reducedMotion?: 'reduce' | 'no-preference' | null, forcedColors?: 'active' | 'none' | null } = {}) { await this._channel.emulateMedia({ media: options.media === null ? 'no-override' : options.media, diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index b67edcbca8..abea7f8fce 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1122,6 +1122,8 @@ scheme.PageGoForwardParams = tObject({ scheme.PageGoForwardResult = tObject({ response: tOptional(tChannel(['Response'])), }); +scheme.PageForceGarbageCollectionParams = tOptional(tObject({})); +scheme.PageForceGarbageCollectionResult = tOptional(tObject({})); scheme.PageRegisterLocatorHandlerParams = tObject({ selector: tString, noWaitAfter: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index f06924d70f..c2d499bd67 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -323,6 +323,10 @@ export class BidiPage implements PageDelegate { throw new Error('Method not implemented.'); } + async forceGarbageCollection(): Promise { + throw new Error('Method not implemented.'); + } + async addInitScript(initScript: InitScript): Promise { const { script } = await this._session.send('script.addPreloadScript', { // TODO: remove function call from the source. diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 5a7fb5e4af..fbdc9db91a 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -247,6 +247,10 @@ export class CRPage implements PageDelegate { return this._go(+1); } + async forceGarbageCollection(): Promise { + await this._mainFrameSession._client.send('HeapProfiler.collectGarbage'); + } + async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise { await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world)); } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 3101cd051d..a97ddbf1f0 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -137,6 +137,10 @@ export class PageDispatcher extends Dispatcher { + await this._page.forceGarbageCollection(); + } + async registerLocatorHandler(params: channels.PageRegisterLocatorHandlerParams, metadata: CallMetadata): Promise { const uid = this._page.registerLocatorHandler(params.selector, params.noWaitAfter); return { uid }; diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index d1066876eb..03a27954dd 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -400,6 +400,10 @@ export class FFPage implements PageDelegate { return success; } + async forceGarbageCollection(): Promise { + await this._session.send('Heap.collectGarbage'); + } + async addInitScript(initScript: InitScript, worldName?: string): Promise { this._initScripts.push({ initScript, worldName }); await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) }); diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index f2e59aa56f..aeaeb0af88 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -54,6 +54,7 @@ export interface PageDelegate { reload(): Promise; goBack(): Promise; goForward(): Promise; + forceGarbageCollection(): Promise; addInitScript(initScript: InitScript): Promise; removeNonInternalInitScripts(): Promise; closePage(runBeforeUnload: boolean): Promise; @@ -430,6 +431,10 @@ export class Page extends SdkObject { }), this._timeoutSettings.navigationTimeout(options)); } + forceGarbageCollection(): Promise { + return this._delegate.forceGarbageCollection(); + } + registerLocatorHandler(selector: string, noWaitAfter: boolean | undefined) { const uid = ++this._lastLocatorHandlerUid; this._locatorHandlers.set(uid, { selector, noWaitAfter }); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index c3954b4882..2f579b619b 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -768,6 +768,10 @@ export class WKPage implements PageDelegate { }); } + async forceGarbageCollection(): Promise { + await this._session.send('Heap.gc'); + } + async addInitScript(initScript: InitScript): Promise { await this._updateBootstrapScript(); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 1c3b7f50b2..80d99a732b 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2554,6 +2554,11 @@ export interface Page { timeout?: number; }): Promise; + /** + * Force the browser to perform garbage collection. + */ + forceGarbageCollection(): Promise; + /** * Returns frame matching the specified criteria. Either `name` or `url` must be specified. * diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 143f1ad0e0..689f0275b1 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1933,6 +1933,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { exposeBinding(params: PageExposeBindingParams, metadata?: CallMetadata): Promise; goBack(params: PageGoBackParams, metadata?: CallMetadata): Promise; goForward(params: PageGoForwardParams, metadata?: CallMetadata): Promise; + forceGarbageCollection(params?: PageForceGarbageCollectionParams, metadata?: CallMetadata): Promise; registerLocatorHandler(params: PageRegisterLocatorHandlerParams, metadata?: CallMetadata): Promise; resolveLocatorHandlerNoReply(params: PageResolveLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise; unregisterLocatorHandler(params: PageUnregisterLocatorHandlerParams, metadata?: CallMetadata): Promise; @@ -2070,6 +2071,9 @@ export type PageGoForwardOptions = { export type PageGoForwardResult = { response?: ResponseChannel, }; +export type PageForceGarbageCollectionParams = {}; +export type PageForceGarbageCollectionOptions = {}; +export type PageForceGarbageCollectionResult = void; export type PageRegisterLocatorHandlerParams = { selector: string, noWaitAfter?: boolean, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 4f064ffa08..ce206ab569 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1430,6 +1430,8 @@ Page: slowMo: true snapshot: true + forceGarbageCollection: + registerLocatorHandler: parameters: selector: string diff --git a/tests/page/page-force-gc.spec.ts b/tests/page/page-force-gc.spec.ts new file mode 100644 index 0000000000..038d471eba --- /dev/null +++ b/tests/page/page-force-gc.spec.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2024 Adobe Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './pageTest'; + +test('should work', async ({ page }) => { + await page.evaluate(() => { + globalThis.objectToDestroy = {}; + globalThis.weakRef = new WeakRef(globalThis.objectToDestroy); + }); + await page.evaluate(() => globalThis.objectToDestroy = null); + await page.forceGarbageCollection(); + expect(await page.evaluate(() => globalThis.weakRef.deref())).toBe(undefined); +}); From 34876e929108fb13476b00e0e038ca020db909fa Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 13 Sep 2024 18:29:35 -0700 Subject: [PATCH 09/12] chore: cookies in intercepted bidi requests (#32623) --- .../src/server/bidi/bidiNetworkManager.ts | 43 +++++++-- .../playwright-core/src/server/cookieStore.ts | 92 +++++++++++++++++++ packages/playwright-core/src/server/fetch.ts | 81 +--------------- tests/config/browserTest.ts | 8 +- tests/page/pageTestApi.ts | 2 +- 5 files changed, 139 insertions(+), 87 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts index 00846b124a..b7c314bd10 100644 --- a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts +++ b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts @@ -22,6 +22,7 @@ import type * as frames from '../frames'; import type * as types from '../types'; import * as bidi from './third_party/bidiProtocol'; import type { BidiSession } from './bidiConnection'; +import { parseRawCookie } from '../cookieStore'; export class BidiNetworkManager { @@ -68,7 +69,7 @@ export class BidiNetworkManager { if (redirectedFrom) { this._session.sendMayFail('network.continueRequest', { request: param.request.request, - headers: redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders, + ...(redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders || {}), }); } else { route = new BidiRouteImpl(this._session, param.request.request); @@ -245,7 +246,7 @@ class BidiRouteImpl implements network.RouteDelegate { private _requestId: bidi.Network.Request; private _session: BidiSession; private _request!: network.Request; - _alreadyContinuedHeaders: bidi.Network.Header[] | undefined; + _alreadyContinuedHeaders: types.HeadersArray | undefined; constructor(session: BidiSession, requestId: bidi.Network.Request) { this._session = session; @@ -266,13 +267,12 @@ class BidiRouteImpl implements network.RouteDelegate { return header; }); } - this._alreadyContinuedHeaders = toBidiHeaders(headers); + this._alreadyContinuedHeaders = headers; await this._session.sendMayFail('network.continueRequest', { request: this._requestId, url: overrides.url, method: overrides.method, - // TODO: cookies! - headers: this._alreadyContinuedHeaders, + ...toBidiRequestHeaders(this._alreadyContinuedHeaders), body: overrides.postData ? { type: 'base64', value: Buffer.from(overrides.postData).toString('base64') } : undefined, }); } @@ -283,7 +283,7 @@ class BidiRouteImpl implements network.RouteDelegate { request: this._requestId, statusCode: response.status, reasonPhrase: network.statusText(response.status), - headers: toBidiHeaders(response.headers), + ...toBidiResponseHeaders(response.headers), body: { type: 'base64', value: base64body }, }); } @@ -302,6 +302,27 @@ function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray return result; } +function toBidiRequestHeaders(allHeaders: types.HeadersArray): { cookies: bidi.Network.CookieHeader[], headers: bidi.Network.Header[] } { + const bidiHeaders = toBidiHeaders(allHeaders); + const cookies = bidiHeaders.filter(h => h.name.toLowerCase() === 'cookie'); + const headers = bidiHeaders.filter(h => h.name.toLowerCase() !== 'cookie'); + return { cookies, headers }; +} + +function toBidiResponseHeaders(headers: types.HeadersArray): { cookies: bidi.Network.SetCookieHeader[], headers: bidi.Network.Header[] } { + const setCookieHeaders = headers.filter(h => h.name.toLowerCase() === 'set-cookie'); + const otherHeaders = headers.filter(h => h.name.toLowerCase() !== 'set-cookie'); + const rawCookies = setCookieHeaders.map(h => parseRawCookie(h.value)); + const cookies: bidi.Network.SetCookieHeader[] = rawCookies.filter(Boolean).map(c => { + return { + ...c!, + value: { type: 'string', value: c!.value }, + sameSite: toBidiSameSite(c!.sameSite), + }; + }); + return { cookies, headers: toBidiHeaders(otherHeaders) }; +} + function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] { return headers.map(({ name, value }) => ({ name, value: { type: 'string', value } })); } @@ -314,3 +335,13 @@ export function bidiBytesValueToString(value: bidi.Network.BytesValue): string { return 'unknown value type: ' + (value as any).type; } + +function toBidiSameSite(sameSite?: 'Strict' | 'Lax' | 'None'): bidi.Network.SameSite | undefined { + if (!sameSite) + return undefined; + if (sameSite === 'Strict') + return bidi.Network.SameSite.Strict; + if (sameSite === 'Lax') + return bidi.Network.SameSite.Lax; + return bidi.Network.SameSite.None; +} diff --git a/packages/playwright-core/src/server/cookieStore.ts b/packages/playwright-core/src/server/cookieStore.ts index fbf3f718f0..d1842660c7 100644 --- a/packages/playwright-core/src/server/cookieStore.ts +++ b/packages/playwright-core/src/server/cookieStore.ts @@ -15,6 +15,7 @@ */ import type * as channels from '@protocol/channels'; +import { kMaxCookieExpiresDateInSeconds } from './network'; class Cookie { private _raw: channels.NetworkCookie; @@ -115,6 +116,97 @@ export class CookieStore { } } +type RawCookie = { + name: string, + value: string, + domain?: string, + path?: string, + expires?: number, + httpOnly?: boolean, + secure?: boolean, + sameSite?: 'Strict' | 'Lax' | 'None', +}; + +export function parseRawCookie(header: string): RawCookie | null { + const pairs = header.split(';').filter(s => s.trim().length > 0).map(p => { + let key = ''; + let value = ''; + const separatorPos = p.indexOf('='); + if (separatorPos === -1) { + // If only a key is specified, the value is left undefined. + key = p.trim(); + } else { + // Otherwise we assume that the key is the element before the first `=` + key = p.slice(0, separatorPos).trim(); + // And the value is the rest of the string. + value = p.slice(separatorPos + 1).trim(); + } + return [key, value]; + }); + if (!pairs.length) + return null; + const [name, value] = pairs[0]; + const cookie: RawCookie = { + name, + value, + }; + for (let i = 1; i < pairs.length; i++) { + const [name, value] = pairs[i]; + switch (name.toLowerCase()) { + case 'expires': + const expiresMs = (+new Date(value)); + // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1 + if (isFinite(expiresMs)) { + if (expiresMs <= 0) + cookie.expires = 0; + else + cookie.expires = Math.min(expiresMs / 1000, kMaxCookieExpiresDateInSeconds); + } + break; + case 'max-age': + const maxAgeSec = parseInt(value, 10); + if (isFinite(maxAgeSec)) { + // From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2 + // If delta-seconds is less than or equal to zero (0), let expiry-time + // be the earliest representable date and time. + if (maxAgeSec <= 0) + cookie.expires = 0; + else + cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds); + } + break; + case 'domain': + cookie.domain = value.toLocaleLowerCase() || ''; + if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.')) + cookie.domain = '.' + cookie.domain; + break; + case 'path': + cookie.path = value || ''; + break; + case 'secure': + cookie.secure = true; + break; + case 'httponly': + cookie.httpOnly = true; + break; + case 'samesite': + switch (value.toLowerCase()) { + case 'none': + cookie.sameSite = 'None'; + break; + case 'lax': + cookie.sameSite = 'Lax'; + break; + case 'strict': + cookie.sameSite = 'Strict'; + break; + } + break; + } + } + return cookie; +} + export function domainMatches(value: string, domain: string): boolean { if (value === domain) return true; diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 5d00dc05a6..01c6c397ab 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -28,7 +28,7 @@ import { getUserAgent } from '../utils/userAgent'; import { assert, createGuid, monotonicTime } from '../utils'; import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle'; import { BrowserContext, verifyClientCertificates } from './browserContext'; -import { CookieStore, domainMatches } from './cookieStore'; +import { CookieStore, domainMatches, parseRawCookie } from './cookieStore'; import { MultipartFormData } from './formData'; import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs'; import type { CallMetadata } from './instrumentation'; @@ -39,7 +39,6 @@ import { ProgressController } from './progress'; import { Tracing } from './trace/recorder/tracing'; import type * as types from './types'; import type { HeadersArray, ProxySettings } from './types'; -import { kMaxCookieExpiresDateInSeconds } from './network'; import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; type FetchRequestOptions = { @@ -640,27 +639,10 @@ function toHeadersArray(rawHeaders: string[]): types.HeadersArray { const redirectStatus = [301, 302, 303, 307, 308]; function parseCookie(header: string): channels.NetworkCookie | null { - const pairs = header.split(';').filter(s => s.trim().length > 0).map(p => { - let key = ''; - let value = ''; - const separatorPos = p.indexOf('='); - if (separatorPos === -1) { - // If only a key is specified, the value is left undefined. - key = p.trim(); - } else { - // Otherwise we assume that the key is the element before the first `=` - key = p.slice(0, separatorPos).trim(); - // And the value is the rest of the string. - value = p.slice(separatorPos + 1).trim(); - } - return [key, value]; - }); - if (!pairs.length) + const raw = parseRawCookie(header); + if (!raw) return null; - const [name, value] = pairs[0]; const cookie: channels.NetworkCookie = { - name, - value, domain: '', path: '', expires: -1, @@ -668,62 +650,9 @@ function parseCookie(header: string): channels.NetworkCookie | null { secure: false, // From https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite // The cookie-sending behavior if SameSite is not specified is SameSite=Lax. - sameSite: 'Lax' + sameSite: 'Lax', + ...raw }; - for (let i = 1; i < pairs.length; i++) { - const [name, value] = pairs[i]; - switch (name.toLowerCase()) { - case 'expires': - const expiresMs = (+new Date(value)); - // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1 - if (isFinite(expiresMs)) { - if (expiresMs <= 0) - cookie.expires = 0; - else - cookie.expires = Math.min(expiresMs / 1000, kMaxCookieExpiresDateInSeconds); - } - break; - case 'max-age': - const maxAgeSec = parseInt(value, 10); - if (isFinite(maxAgeSec)) { - // From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2 - // If delta-seconds is less than or equal to zero (0), let expiry-time - // be the earliest representable date and time. - if (maxAgeSec <= 0) - cookie.expires = 0; - else - cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds); - } - break; - case 'domain': - cookie.domain = value.toLocaleLowerCase() || ''; - if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.')) - cookie.domain = '.' + cookie.domain; - break; - case 'path': - cookie.path = value || ''; - break; - case 'secure': - cookie.secure = true; - break; - case 'httponly': - cookie.httpOnly = true; - break; - case 'samesite': - switch (value.toLowerCase()) { - case 'none': - cookie.sameSite = 'None'; - break; - case 'lax': - cookie.sameSite = 'Lax'; - break; - case 'strict': - cookie.sameSite = 'Strict'; - break; - } - break; - } - } return cookie; } diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 196f3604db..44aaca9d26 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -62,21 +62,21 @@ const test = baseTest.extend await run(playwright[browserName]); }, { scope: 'worker' }], - allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => { + allowsThirdParty: [async ({ browserName }, run) => { if (browserName === 'firefox') await run(true); else await run(false); }, { scope: 'worker' }], - defaultSameSiteCookieValue: [async ({ browserName, browserMajorVersion, channel, isLinux }, run) => { - if (browserName === 'chromium') + defaultSameSiteCookieValue: [async ({ browserName, isLinux }, run) => { + if (browserName === 'chromium' || browserName as any === '_bidiChromium') await run('Lax'); else if (browserName === 'webkit' && isLinux) await run('Lax'); else if (browserName === 'webkit' && !isLinux) await run('None'); - else if (browserName === 'firefox') + else if (browserName === 'firefox' || browserName as any === '_bidiFirefox') await run('None'); else throw new Error('unknown browser - ' + browserName); diff --git a/tests/page/pageTestApi.ts b/tests/page/pageTestApi.ts index cf497e76c0..1ccfd608e9 100644 --- a/tests/page/pageTestApi.ts +++ b/tests/page/pageTestApi.ts @@ -29,7 +29,7 @@ export type PageWorkerFixtures = { screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick; trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'retain-on-first-failure' | 'on-all-retries' | /** deprecated */ 'retry-with-trace'; video: VideoMode | { mode: VideoMode, size: ViewportSize }; - browserName: 'chromium' | 'firefox' | 'webkit'; + browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium'; browserVersion: string; browserMajorVersion: number; electronMajorVersion: number; From aeb4d182f749f8185797b66f28bff6828d35b867 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Sat, 14 Sep 2024 10:17:07 +0200 Subject: [PATCH 10/12] feat(tracing): add .pwtrace to trace file extension (#32581) Closes https://github.com/microsoft/playwright/issues/32226 I've updated every mention of `.trace.zip` except for the release notes. --- docs/src/api/class-tracing.md | 40 +++++----- docs/src/test-global-setup-teardown-js.md | 4 +- docs/src/trace-viewer-intro-java-python.md | 14 ++-- docs/src/trace-viewer-intro-js.md | 2 +- docs/src/trace-viewer.md | 32 ++++---- packages/playwright-core/src/cli/program.ts | 2 +- .../src/server/trace/recorder/tracing.ts | 2 +- packages/playwright-core/types/types.d.ts | 8 +- packages/playwright/src/worker/testTracing.ts | 4 +- tests/config/traceViewerFixtures.ts | 2 +- .../playwright-electron-should-work.spec.ts | 6 +- tests/library/browsercontext-reuse.spec.ts | 2 +- .../library/chromium/connect-over-cdp.spec.ts | 2 +- tests/library/inspector/cli-codegen-2.spec.ts | 4 +- tests/library/trace-viewer.spec.ts | 14 ++-- tests/library/tracing.spec.ts | 56 ++++++------- tests/library/video.spec.ts | 2 +- .../playwright.artifacts.spec.ts | 70 ++++++++-------- .../playwright-test/playwright.reuse.spec.ts | 8 +- .../playwright-test/playwright.trace.spec.ts | 80 +++++++++---------- .../reporter-attachment.spec.ts | 6 +- 21 files changed, 180 insertions(+), 180 deletions(-) diff --git a/docs/src/api/class-tracing.md b/docs/src/api/class-tracing.md index 6e7541e4cb..f35e9e53ff 100644 --- a/docs/src/api/class-tracing.md +++ b/docs/src/api/class-tracing.md @@ -11,7 +11,7 @@ const context = await browser.newContext(); await context.tracing.start({ screenshots: true, snapshots: true }); const page = await context.newPage(); await page.goto('https://playwright.dev'); -await context.tracing.stop({ path: 'trace.zip' }); +await context.tracing.stop({ path: 'trace.pwtrace.zip' }); ``` ```java @@ -23,7 +23,7 @@ context.tracing().start(new Tracing.StartOptions() Page page = context.newPage(); page.navigate("https://playwright.dev"); context.tracing().stop(new Tracing.StopOptions() - .setPath(Paths.get("trace.zip"))); + .setPath(Paths.get("trace.pwtrace.zip"))); ``` ```python async @@ -32,7 +32,7 @@ context = await browser.new_context() await context.tracing.start(screenshots=True, snapshots=True) page = await context.new_page() await page.goto("https://playwright.dev") -await context.tracing.stop(path = "trace.zip") +await context.tracing.stop(path = "trace.pwtrace.zip") ``` ```python sync @@ -41,7 +41,7 @@ context = browser.new_context() context.tracing.start(screenshots=True, snapshots=True) page = context.new_page() page.goto("https://playwright.dev") -context.tracing.stop(path = "trace.zip") +context.tracing.stop(path = "trace.pwtrace.zip") ``` ```csharp @@ -57,7 +57,7 @@ var page = await context.NewPageAsync(); await page.GotoAsync("https://playwright.dev"); await context.Tracing.StopAsync(new() { - Path = "trace.zip" + Path = "trace.pwtrace.zip" }); ``` @@ -72,7 +72,7 @@ Start tracing. await context.tracing.start({ screenshots: true, snapshots: true }); const page = await context.newPage(); await page.goto('https://playwright.dev'); -await context.tracing.stop({ path: 'trace.zip' }); +await context.tracing.stop({ path: 'trace.pwtrace.zip' }); ``` ```java @@ -82,21 +82,21 @@ context.tracing().start(new Tracing.StartOptions() Page page = context.newPage(); page.navigate("https://playwright.dev"); context.tracing().stop(new Tracing.StopOptions() - .setPath(Paths.get("trace.zip"))); + .setPath(Paths.get("trace.pwtrace.zip"))); ``` ```python async await context.tracing.start(screenshots=True, snapshots=True) page = await context.new_page() await page.goto("https://playwright.dev") -await context.tracing.stop(path = "trace.zip") +await context.tracing.stop(path = "trace.pwtrace.zip") ``` ```python sync context.tracing.start(screenshots=True, snapshots=True) page = context.new_page() page.goto("https://playwright.dev") -context.tracing.stop(path = "trace.zip") +context.tracing.stop(path = "trace.pwtrace.zip") ``` ```csharp @@ -112,7 +112,7 @@ var page = await context.NewPageAsync(); await page.GotoAsync("https://playwright.dev"); await context.Tracing.StopAsync(new() { - Path = "trace.zip" + Path = "trace.pwtrace.zip" }); ``` @@ -177,12 +177,12 @@ await page.goto('https://playwright.dev'); await context.tracing.startChunk(); await page.getByText('Get Started').click(); // Everything between startChunk and stopChunk will be recorded in the trace. -await context.tracing.stopChunk({ path: 'trace1.zip' }); +await context.tracing.stopChunk({ path: 'trace1.pwtrace.zip' }); await context.tracing.startChunk(); await page.goto('http://example.com'); // Save a second trace file with different actions. -await context.tracing.stopChunk({ path: 'trace2.zip' }); +await context.tracing.stopChunk({ path: 'trace2.pwtrace.zip' }); ``` ```java @@ -196,13 +196,13 @@ context.tracing().startChunk(); page.getByText("Get Started").click(); // Everything between startChunk and stopChunk will be recorded in the trace. context.tracing().stopChunk(new Tracing.StopChunkOptions() - .setPath(Paths.get("trace1.zip"))); + .setPath(Paths.get("trace1.pwtrace.zip"))); context.tracing().startChunk(); page.navigate("http://example.com"); // Save a second trace file with different actions. context.tracing().stopChunk(new Tracing.StopChunkOptions() - .setPath(Paths.get("trace2.zip"))); + .setPath(Paths.get("trace2.pwtrace.zip"))); ``` ```python async @@ -213,12 +213,12 @@ await page.goto("https://playwright.dev") await context.tracing.start_chunk() await page.get_by_text("Get Started").click() # Everything between start_chunk and stop_chunk will be recorded in the trace. -await context.tracing.stop_chunk(path = "trace1.zip") +await context.tracing.stop_chunk(path = "trace1.pwtrace.zip") await context.tracing.start_chunk() await page.goto("http://example.com") # Save a second trace file with different actions. -await context.tracing.stop_chunk(path = "trace2.zip") +await context.tracing.stop_chunk(path = "trace2.pwtrace.zip") ``` ```python sync @@ -229,12 +229,12 @@ page.goto("https://playwright.dev") context.tracing.start_chunk() page.get_by_text("Get Started").click() # Everything between start_chunk and stop_chunk will be recorded in the trace. -context.tracing.stop_chunk(path = "trace1.zip") +context.tracing.stop_chunk(path = "trace1.pwtrace.zip") context.tracing.start_chunk() page.goto("http://example.com") # Save a second trace file with different actions. -context.tracing.stop_chunk(path = "trace2.zip") +context.tracing.stop_chunk(path = "trace2.pwtrace.zip") ``` ```csharp @@ -254,7 +254,7 @@ await page.GetByText("Get Started").ClickAsync(); // Everything between StartChunkAsync and StopChunkAsync will be recorded in the trace. await context.Tracing.StopChunkAsync(new() { - Path = "trace1.zip" + Path = "trace1.pwtrace.zip" }); await context.Tracing.StartChunkAsync(); @@ -262,7 +262,7 @@ await page.GotoAsync("http://example.com"); // Save a second trace file with different actions. await context.Tracing.StopChunkAsync(new() { - Path = "trace2.zip" + Path = "trace2.pwtrace.zip" }); ``` diff --git a/docs/src/test-global-setup-teardown-js.md b/docs/src/test-global-setup-teardown-js.md index 883bdf25d6..0bb05cd78d 100644 --- a/docs/src/test-global-setup-teardown-js.md +++ b/docs/src/test-global-setup-teardown-js.md @@ -238,12 +238,12 @@ async function globalSetup(config: FullConfig) { await page.getByText('Sign in').click(); await context.storageState({ path: storageState as string }); await context.tracing.stop({ - path: './test-results/setup-trace.zip', + path: './test-results/setup-trace.pwtrace.zip', }); await browser.close(); } catch (error) { await context.tracing.stop({ - path: './test-results/failed-setup-trace.zip', + path: './test-results/failed-setup-trace.pwtrace.zip', }); await browser.close(); throw error; diff --git a/docs/src/trace-viewer-intro-java-python.md b/docs/src/trace-viewer-intro-java-python.md index 79a415e54a..db4f096b9e 100644 --- a/docs/src/trace-viewer-intro-java-python.md +++ b/docs/src/trace-viewer-intro-java-python.md @@ -25,7 +25,7 @@ Options for tracing are: - `off`: Do not record trace. (default) - `retain-on-failure`: Record trace for each test, but remove all traces from successful test runs. -This will record the trace and place it into the file named `trace.zip` in your `test-results` directory. +This will record the trace and place it into the file named `trace.pwtrace.zip` in your `test-results` directory.
If you are not using Pytest, click here to learn how to record traces. @@ -41,7 +41,7 @@ page = await context.new_page() await page.goto("https://playwright.dev") # Stop tracing and export it into a zip archive. -await context.tracing.stop(path = "trace.zip") +await context.tracing.stop(path = "trace.pwtrace.zip") ``` ```python sync @@ -55,7 +55,7 @@ page = context.new_page() page.goto("https://playwright.dev") # Stop tracing and export it into a zip archive. -context.tracing.stop(path = "trace.zip") +context.tracing.stop(path = "trace.pwtrace.zip") ```
@@ -80,22 +80,22 @@ page.navigate("https://playwright.dev"); // Stop tracing and export it into a zip archive. context.tracing().stop(new Tracing.StopOptions() - .setPath(Paths.get("trace.zip"))); + .setPath(Paths.get("trace.pwtrace.zip"))); ``` -This will record the trace and place it into the file named `trace.zip`. +This will record the trace and place it into the file named `trace.pwtrace.zip`. ## Opening the trace You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your trace's zip file is located. Once opened you can click on each action or use the timeline to see the state of the page before and after each action. You can also inspect the log, source and network during each step of the test. The trace viewer creates a DOM snapshot so you can fully interact with it, open devtools etc. ```bash java -mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip" +mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.pwtrace.zip" ``` ```bash python -playwright show-trace trace.zip +playwright show-trace trace.pwtrace.zip ``` ###### diff --git a/docs/src/trace-viewer-intro-js.md b/docs/src/trace-viewer-intro-js.md index a950689468..b7ac93dd9c 100644 --- a/docs/src/trace-viewer-intro-js.md +++ b/docs/src/trace-viewer-intro-js.md @@ -22,7 +22,7 @@ Playwright Trace Viewer is a GUI tool that lets you explore recorded Playwright ## Recording a Trace -By default the [playwright.config](./trace-viewer.md#recording-a-trace-on-ci) file will contain the configuration needed to create a `trace.zip` file for each test. Traces are setup to run `on-first-retry` meaning they will be run on the first retry of a failed test. Also `retries` are set to 2 when running on CI and 0 locally. This means the traces will be recorded on the first retry of a failed test but not on the first run and not on the second retry. +By default the [playwright.config](./trace-viewer.md#recording-a-trace-on-ci) file will contain the configuration needed to create a `trace.pwtrace.zip` file for each test. Traces are setup to run `on-first-retry` meaning they will be run on the first retry of a failed test. Also `retries` are set to 2 when running on CI and 0 locally. This means the traces will be recorded on the first retry of a failed test but not on the first run and not on the second retry. ```js title="playwright.config.ts" import { defineConfig } from '@playwright/test'; diff --git a/docs/src/trace-viewer.md b/docs/src/trace-viewer.md index 207646c1d7..ad26278930 100644 --- a/docs/src/trace-viewer.md +++ b/docs/src/trace-viewer.md @@ -132,7 +132,7 @@ npx playwright show-report * langs: js Traces should be run on continuous integration on the first retry of a failed test -by setting the `trace: 'on-first-retry'` option in the test configuration file. This will produce a `trace.zip` file for each test that was retried. +by setting the `trace: 'on-first-retry'` option in the test configuration file. This will produce a `trace.pwtrace.zip` file for each test that was retried. ```js tab=js-test title="playwright.config.ts" import { defineConfig } from '@playwright/test'; @@ -155,7 +155,7 @@ const page = await context.newPage(); await page.goto('https://playwright.dev'); // Stop tracing and export it into a zip archive. -await context.tracing.stop({ path: 'trace.zip' }); +await context.tracing.stop({ path: 'trace.pwtrace.zip' }); ``` Available options to record a trace: @@ -185,7 +185,7 @@ Options for tracing are: - `off`: Do not record trace. (default) - `retain-on-failure`: Record trace for each test, but remove all traces from successful test runs. -This will record the trace and place it into the file named `trace.zip` in your `test-results` directory. +This will record the trace and place it into the file named `trace.pwtrace.zip` in your `test-results` directory.
If you are not using Pytest, click here to learn how to record traces. @@ -201,7 +201,7 @@ page = await context.new_page() await page.goto("https://playwright.dev") # Stop tracing and export it into a zip archive. -await context.tracing.stop(path = "trace.zip") +await context.tracing.stop(path = "trace.pwtrace.zip") ``` ```python sync @@ -215,7 +215,7 @@ page = context.new_page() page.goto("https://playwright.dev") # Stop tracing and export it into a zip archive. -context.tracing.stop(path = "trace.zip") +context.tracing.stop(path = "trace.pwtrace.zip") ```
@@ -240,10 +240,10 @@ page.navigate("https://playwright.dev"); // Stop tracing and export it into a zip archive. context.tracing().stop(new Tracing.StopOptions() - .setPath(Paths.get("trace.zip"))); + .setPath(Paths.get("trace.pwtracezip"))); ``` -This will record the trace and place it into the file named `trace.zip`. +This will record the trace and place it into the file named `trace.pwtrace.zip`. ## Recording a trace * langs: csharp @@ -466,22 +466,22 @@ public class ExampleTest : PageTest ## Opening the trace -You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your `trace.zip` file is located. +You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your `trace.pwtrace.zip` file is located. ```bash js -npx playwright show-trace path/to/trace.zip +npx playwright show-trace path/to/trace.pwtrace.zip ``` ```bash java -mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip" +mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.pwtrace.zip" ``` ```bash python -playwright show-trace trace.zip +playwright show-trace trace.pwtrace.zip ``` ```bash csharp -pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip +pwsh bin/Debug/netX/playwright.ps1 show-trace trace.pwtrace.zip ``` ## Using [trace.playwright.dev](https://trace.playwright.dev) @@ -496,19 +496,19 @@ pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip You can open remote traces using its URL. They could be generated on a CI run which makes it easy to view the remote trace without having to manually download the file. ```bash js -npx playwright show-trace https://example.com/trace.zip +npx playwright show-trace https://example.com/trace.pwtrace.zip ``` ```bash java -mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace https://example.com/trace.zip" +mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace https://example.com/trace.pwtrace.zip" ``` ```bash python -playwright show-trace https://example.com/trace.zip +playwright show-trace https://example.com/trace.pwtrace.zip ``` ```bash csharp -pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.zip +pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.pwtrace.zip ``` diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index d8fa8230c6..6a36210f8e 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -318,7 +318,7 @@ program }).addHelpText('afterAll', ` Examples: - $ show-trace https://example.com/trace.zip`); + $ show-trace https://example.com/trace.pwtrace.zip`); type Options = { browser: string; diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index b09bbe3134..13f15b6222 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -310,7 +310,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._fs.copyFile(this._state.networkFile, newNetworkFile); - const zipFileName = this._state.traceFile + '.zip'; + const zipFileName = this._state.traceFile + '.pwtrace.zip'; if (params.mode === 'archive') this._fs.zip(entries, zipFileName); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 80d99a732b..e88e1b473b 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -19930,7 +19930,7 @@ export interface Touchscreen { * await context.tracing.start({ screenshots: true, snapshots: true }); * const page = await context.newPage(); * await page.goto('https://playwright.dev'); - * await context.tracing.stop({ path: 'trace.zip' }); + * await context.tracing.stop({ path: 'trace.pwtrace.zip' }); * ``` * */ @@ -19944,7 +19944,7 @@ export interface Tracing { * await context.tracing.start({ screenshots: true, snapshots: true }); * const page = await context.newPage(); * await page.goto('https://playwright.dev'); - * await context.tracing.stop({ path: 'trace.zip' }); + * await context.tracing.stop({ path: 'trace.pwtrace.zip' }); * ``` * * @param options @@ -19999,12 +19999,12 @@ export interface Tracing { * await context.tracing.startChunk(); * await page.getByText('Get Started').click(); * // Everything between startChunk and stopChunk will be recorded in the trace. - * await context.tracing.stopChunk({ path: 'trace1.zip' }); + * await context.tracing.stopChunk({ path: 'trace1.pwtrace.zip' }); * * await context.tracing.startChunk(); * await page.goto('http://example.com'); * // Save a second trace file with different actions. - * await context.tracing.stopChunk({ path: 'trace2.zip' }); + * await context.tracing.stopChunk({ path: 'trace2.pwtrace.zip' }); * ``` * * @param options diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index fed7fdde7e..1b58c5ebf6 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -131,7 +131,7 @@ export class TestTracing { } generateNextTraceRecordingPath() { - const file = path.join(this._artifactsDir, createGuid() + '.zip'); + const file = path.join(this._artifactsDir, createGuid() + '.pwtrace.zip'); this._temporaryTraceFiles.push(file); return file; } @@ -214,7 +214,7 @@ export class TestTracing { }); }); - const tracePath = this._testInfo.outputPath('trace.zip'); + const tracePath = this._testInfo.outputPath('trace.pwtrace.zip'); await mergeTraceFiles(tracePath, this._temporaryTraceFiles); this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' }); } diff --git a/tests/config/traceViewerFixtures.ts b/tests/config/traceViewerFixtures.ts index 3b79a55e98..936c8ea998 100644 --- a/tests/config/traceViewerFixtures.ts +++ b/tests/config/traceViewerFixtures.ts @@ -128,7 +128,7 @@ export const traceViewerFixtures: Fixtures { await use(async (body: () => Promise, optsOverrides = {}) => { - const traceFile = testInfo.outputPath('trace.zip'); + const traceFile = testInfo.outputPath('trace.pwtrace.zip'); await context.tracing.start({ snapshots: true, screenshots: true, sources: true, ...optsOverrides }); await body(); await context.tracing.stop({ path: traceFile }); diff --git a/tests/installation/playwright-electron-should-work.spec.ts b/tests/installation/playwright-electron-should-work.spec.ts index 5c6ced0948..32b529224f 100755 --- a/tests/installation/playwright-electron-should-work.spec.ts +++ b/tests/installation/playwright-electron-should-work.spec.ts @@ -58,7 +58,7 @@ test('should work when wrapped inside @playwright/test and trace is enabled', as await expect(window).toHaveTitle(/Playwright/); await expect(window.getByRole('heading')).toHaveText('Playwright'); - const path = test.info().outputPath('electron-trace.zip'); + const path = test.info().outputPath('electron-trace.pwtrace.zip'); if (trace) { await window.context().tracing.stop({ path }); test.info().attachments.push({ name: 'trace', path, contentType: 'application/zip' }); @@ -73,9 +73,9 @@ test('should work when wrapped inside @playwright/test and trace is enabled', as }); const traces = [ // our actual trace. - path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'electron-trace.zip'), + path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'electron-trace.pwtrace.zip'), // contains the expect() calls - path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'trace.zip'), + path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'trace.pwtrace.zip'), ]; for (const trace of traces) expect(fs.existsSync(trace)).toBe(true); diff --git a/tests/library/browsercontext-reuse.spec.ts b/tests/library/browsercontext-reuse.spec.ts index 14590a3ae3..4f7c3e611c 100644 --- a/tests/library/browsercontext-reuse.spec.ts +++ b/tests/library/browsercontext-reuse.spec.ts @@ -248,7 +248,7 @@ test('should reset tracing', async ({ reusedContext, trace }, testInfo) => { page = context.pages()[0]; await page.evaluate('2 + 2'); - const error = await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') }).catch(e => e); + const error = await context.tracing.stopChunk({ path: testInfo.outputPath('trace.pwtrace.zip') }).catch(e => e); expect(error.message).toContain('Must start tracing before stopping'); }); diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts index a473539e8e..55070f3ec8 100644 --- a/tests/library/chromium/connect-over-cdp.spec.ts +++ b/tests/library/chromium/connect-over-cdp.spec.ts @@ -489,7 +489,7 @@ test('should allow tracing over cdp session', async ({ browserType, trace }, tes await context.tracing.start({ screenshots: true, snapshots: true }); const page = await context.newPage(); await page.evaluate(() => 2 + 2); - const traceZip = testInfo.outputPath('trace.zip'); + const traceZip = testInfo.outputPath('trace.pwtrace.zip'); await context.tracing.stop({ path: traceZip }); await cdpBrowser.close(); expect(fs.existsSync(traceZip)).toBe(true); diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index ef67cd8b93..610d87fc05 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -491,7 +491,7 @@ await page1.GotoAsync("about:blank?foo");`); }); test('should --save-trace', async ({ runCLI }, testInfo) => { - const traceFileName = testInfo.outputPath('trace.zip'); + const traceFileName = testInfo.outputPath('trace.pwtrace.zip'); const cli = runCLI([`--save-trace=${traceFileName}`], { autoExitWhen: ' ', }); @@ -502,7 +502,7 @@ await page1.GotoAsync("about:blank?foo");`); test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => { test.skip(platform === 'win32', 'SIGINT not supported on Windows'); - const traceFileName = testInfo.outputPath('trace.zip'); + const traceFileName = testInfo.outputPath('trace.pwtrace.zip'); const storageFileName = testInfo.outputPath('auth.json'); const harFileName = testInfo.outputPath('har.har'); const cli = runCLI([`--save-trace=${traceFileName}`, `--save-storage=${storageFileName}`, `--save-har=${harFileName}`]); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 2b00949b19..0c6ef86f25 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -74,7 +74,7 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s runBeforeCloseBrowserContext: async () => { await page.hover('body'); await page.close(); - traceFile = path.join(workerInfo.project.outputDir, String(workerInfo.workerIndex), browserName, 'trace.zip'); + traceFile = path.join(workerInfo.project.outputDir, String(workerInfo.workerIndex), browserName, 'trace.pwtrace.zip'); await context.tracing.stop({ path: traceFile }); } }; @@ -698,7 +698,7 @@ test('should handle file URIs', async ({ page, runAndTrace, browserName }) => { }); test('should preserve currentSrc', async ({ browser, server, showTraceViewer }) => { - const traceFile = test.info().outputPath('trace.zip'); + const traceFile = test.info().outputPath('trace.pwtrace.zip'); const page = await browser.newPage({ deviceScaleFactor: 3 }); await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true }); await page.setViewportSize({ width: 300, height: 300 }); @@ -1294,7 +1294,7 @@ test('should highlight locator in iframe while typing', async ({ page, runAndTra }); test('should preserve noscript when javascript is disabled', async ({ browser, server, showTraceViewer }) => { - const traceFile = test.info().outputPath('trace.zip'); + const traceFile = test.info().outputPath('trace.pwtrace.zip'); const page = await browser.newPage({ javaScriptEnabled: false }); await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true }); await page.goto(server.EMPTY_PAGE); @@ -1311,8 +1311,8 @@ test('should preserve noscript when javascript is disabled', async ({ browser, s await expect(frame.getByText('javascript is disabled!')).toBeVisible(); }); -test('should remove noscript by default', async ({ browser, server, showTraceViewer, browserType }) => { - const traceFile = test.info().outputPath('trace.zip'); +test('should remove noscript by default', async ({ browser, server, showTraceViewer }) => { + const traceFile = test.info().outputPath('trace.pwtrace.zip'); const page = await browser.newPage({ javaScriptEnabled: undefined }); await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true }); await page.goto(server.EMPTY_PAGE); @@ -1329,8 +1329,8 @@ test('should remove noscript by default', async ({ browser, server, showTraceVie await expect(frame.getByText('Enable JavaScript to run this app.')).toBeHidden(); }); -test('should remove noscript when javaScriptEnabled is set to true', async ({ browser, server, showTraceViewer, browserType }) => { - const traceFile = test.info().outputPath('trace.zip'); +test('should remove noscript when javaScriptEnabled is set to true', async ({ browser, server, showTraceViewer }) => { + const traceFile = test.info().outputPath('trace.pwtrace.zip'); const page = await browser.newPage({ javaScriptEnabled: true }); await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true }); await page.goto(server.EMPTY_PAGE); diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index c82db377aa..770ea0362c 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -37,9 +37,9 @@ test('should collect trace with resources, but no js', async ({ context, page, s await page.locator('input[type="file"]').setInputFiles(asset('file-to-upload.txt')); await page.waitForTimeout(2000); // Give it some time to produce screenshots. await page.close(); - await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }); - const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.zip')); + const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); expect(events[0].type).toBe('context-options'); expect(actions).toEqual([ 'page.goto', @@ -81,8 +81,8 @@ test('should use the correct apiName for event driven callbacks', async ({ conte }); await page.evaluate(() => alert('yo')); - await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); - const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.zip')); + await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }); + const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); expect(events[0].type).toBe('context-options'); expect(actions).toEqual([ 'page.route', @@ -102,9 +102,9 @@ test('should not collect snapshots by default', async ({ context, page, server } await page.setContent(''); await page.click('"Click"'); await page.close(); - await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }); - const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip')); + const { events } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); expect(events.some(e => e.type === 'frame-snapshot')).toBeFalsy(); expect(events.some(e => e.type === 'resource-snapshot')).toBeFalsy(); }); @@ -113,8 +113,8 @@ test('should not include buffers in the trace', async ({ context, page, server } await context.tracing.start({ snapshots: true }); await page.goto(server.PREFIX + '/empty.html'); await page.screenshot(); - await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); - const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.zip')); + await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }); + const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); const screenshotEvent = actionObjects.find(a => a.apiName === 'page.screenshot'); expect(screenshotEvent.beforeSnapshot).toBeTruthy(); expect(screenshotEvent.afterSnapshot).toBeTruthy(); @@ -129,9 +129,9 @@ test('should exclude internal pages', async ({ browserName, context, page, serve await context.tracing.start(); await context.storageState(); await page.close(); - await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }); - const trace = await parseTraceRaw(testInfo.outputPath('trace.zip')); + const trace = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); const pageIds = new Set(); trace.events.forEach(e => { const pageId = e.pageId; @@ -144,8 +144,8 @@ test('should exclude internal pages', async ({ browserName, context, page, serve test('should include context API requests', async ({ browserName, context, page, server }, testInfo) => { await context.tracing.start({ snapshots: true }); await page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } }); - await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); - const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip')); + await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }); + const { events } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); const postEvent = events.find(e => e.apiName === 'apiRequestContext.post'); expect(postEvent).toBeTruthy(); const harEntry = events.find(e => e.type === 'resource-snapshot'); @@ -428,9 +428,9 @@ for (const params of [ await page.setContent(''); await page.evaluate(() => new Promise(window.builtinRequestAnimationFrame)); } - await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }); - const { events, resources } = await parseTraceRaw(testInfo.outputPath('trace.zip')); + const { events, resources } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); const frames = events.filter(e => e.type === 'screencast-frame'); // Check all frame sizes. @@ -460,10 +460,10 @@ test('should include interrupted actions', async ({ context, page, server }, tes await page.goto(server.EMPTY_PAGE); await page.setContent(''); page.click('"ClickNoButton"').catch(() => {}); - await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }); await context.close(); - const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip')); + const { events } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); const clickEvent = events.find(e => e.apiName === 'page.click'); expect(clickEvent).toBeTruthy(); }); @@ -475,7 +475,7 @@ test('should throw when starting with different options', async ({ context }) => }); test('should throw when stopping without start', async ({ context }, testInfo) => { - const error = await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }).catch(e => e); + const error = await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }).catch(e => e); expect(error.message).toContain('Must start tracing before stopping'); }); @@ -492,7 +492,7 @@ test('should work with multiple chunks', async ({ context, page, server }, testI await page.click('"Click"'); page.click('"ClickNoButton"', { timeout: 0 }).catch(() => {}); await page.evaluate(() => {}); - await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') }); + await context.tracing.stopChunk({ path: testInfo.outputPath('trace.pwtrace.zip') }); await context.tracing.startChunk(); await page.hover('"Click"'); @@ -502,7 +502,7 @@ test('should work with multiple chunks', async ({ context, page, server }, testI await page.click('"Click"'); await context.tracing.stopChunk(); // Should stop without a path. - const trace1 = await parseTraceRaw(testInfo.outputPath('trace.zip')); + const trace1 = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); expect(trace1.events[0].type).toBe('context-options'); expect(trace1.actions).toEqual([ 'page.setContent', @@ -533,7 +533,7 @@ test('should export trace concurrently to second navigation', async ({ context, await page.waitForTimeout(timeout); await Promise.all([ promise, - context.tracing.stop({ path: testInfo.outputPath('trace.zip') }), + context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }), ]); } }); @@ -561,9 +561,9 @@ test('should ignore iframes in head', async ({ context, page, server }, testInfo await context.tracing.start({ screenshots: true, snapshots: true }); await page.click('button'); - await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') }); + await context.tracing.stopChunk({ path: testInfo.outputPath('trace.pwtrace.zip') }); - const trace = await parseTraceRaw(testInfo.outputPath('trace.zip')); + const trace = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); expect(trace.actions).toEqual([ 'page.click', ]); @@ -581,7 +581,7 @@ test('should hide internal stack frames', async ({ context, page }, testInfo) => await page.setContent(`
Click me
`); await page.click('div'); await evalPromise; - const tracePath = testInfo.outputPath('trace.zip'); + const tracePath = testInfo.outputPath('trace.pwtrace.zip'); await context.tracing.stop({ path: tracePath }); const trace = await parseTraceRaw(tracePath); @@ -602,7 +602,7 @@ test('should hide internal stack frames in expect', async ({ context, page }, te await page.click('div'); await expect(page.locator('div')).toBeVisible(); await expectPromise; - const tracePath = testInfo.outputPath('trace.zip'); + const tracePath = testInfo.outputPath('trace.pwtrace.zip'); await context.tracing.stop({ path: tracePath }); const trace = await parseTraceRaw(tracePath); @@ -616,7 +616,7 @@ test('should record global request trace', async ({ request, context, server }, await (request as any)._tracing.start({ snapshots: true }); const url = server.PREFIX + '/simple.json'; await request.get(url); - const tracePath = testInfo.outputPath('trace.zip'); + const tracePath = testInfo.outputPath('trace.pwtrace.zip'); await (request as any)._tracing.stop({ path: tracePath }); const trace = await parseTraceRaw(tracePath); @@ -649,7 +649,7 @@ test('should store global request traces separately', async ({ request, server, request.get(url), request2.post(url) ]); - const tracePath = testInfo.outputPath('trace.zip'); + const tracePath = testInfo.outputPath('trace.pwtrace.zip'); const trace2Path = testInfo.outputPath('trace2.zip'); await Promise.all([ (request as any)._tracing.stop({ path: tracePath }), @@ -682,7 +682,7 @@ test('should store postData for global request', async ({ request, server }, tes await request.post(url, { data: 'test' }); - const tracePath = testInfo.outputPath('trace.zip'); + const tracePath = testInfo.outputPath('trace.pwtrace.zip'); await (request as any)._tracing.stop({ path: tracePath }); const trace = await parseTraceRaw(tracePath); @@ -755,7 +755,7 @@ test('should flush console events on tracing stop', async ({ context, page }, te }); }); await promise; - const tracePath = testInfo.outputPath('trace.zip'); + const tracePath = testInfo.outputPath('trace.pwtrace.zip'); await context.tracing.stop({ path: tracePath }); const trace = await parseTraceRaw(tracePath); const events = trace.events.filter(e => e.type === 'console'); diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index 39dcaecbc6..1413ba1ed5 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -799,7 +799,7 @@ it.describe('screencast', () => { it.fixme(!headless || !!process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW, 'different trace screencast image size on all browsers'); const size = { width: 500, height: 400 }; - const traceFile = testInfo.outputPath('trace.zip'); + const traceFile = testInfo.outputPath('trace.pwtrace.zip'); const context = await browser.newContext({ recordVideo: { diff --git a/tests/playwright-test/playwright.artifacts.spec.ts b/tests/playwright-test/playwright.artifacts.spec.ts index b666aa4b70..cc86c6c584 100644 --- a/tests/playwright-test/playwright.artifacts.spec.ts +++ b/tests/playwright-test/playwright.artifacts.spec.ts @@ -235,25 +235,25 @@ test('should work with trace: on', async ({ runInlineTest }, testInfo) => { expect(listFiles(testInfo.outputPath('test-results'))).toEqual([ '.last-run.json', 'artifacts-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-own-context-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-own-context-passing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-passing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-persistent-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-persistent-passing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-shared-shared-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-shared-shared-passing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-two-contexts', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-two-contexts-failing', - ' trace.zip', + ' trace.pwtrace.zip', ]); }); @@ -271,15 +271,15 @@ test('should work with trace: retain-on-failure', async ({ runInlineTest }, test expect(listFiles(testInfo.outputPath('test-results'))).toEqual([ '.last-run.json', 'artifacts-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-own-context-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-persistent-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-shared-shared-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-two-contexts-failing', - ' trace.zip', + ' trace.pwtrace.zip', ]); }); @@ -297,15 +297,15 @@ test('should work with trace: on-first-retry', async ({ runInlineTest }, testInf expect(listFiles(testInfo.outputPath('test-results'))).toEqual([ '.last-run.json', 'artifacts-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-own-context-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-persistent-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-shared-shared-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-two-contexts-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', ]); }); @@ -323,25 +323,25 @@ test('should work with trace: on-all-retries', async ({ runInlineTest }, testInf expect(listFiles(testInfo.outputPath('test-results'))).toEqual([ '.last-run.json', 'artifacts-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-failing-retry2', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-own-context-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-own-context-failing-retry2', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-persistent-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-persistent-failing-retry2', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-shared-shared-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-shared-shared-failing-retry2', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-two-contexts-failing-retry1', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-two-contexts-failing-retry2', - ' trace.zip', + ' trace.pwtrace.zip', ]); }); @@ -359,15 +359,15 @@ test('should work with trace: retain-on-first-failure', async ({ runInlineTest } expect(listFiles(testInfo.outputPath('test-results'))).toEqual([ '.last-run.json', 'artifacts-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-own-context-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-persistent-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-shared-shared-failing', - ' trace.zip', + ' trace.pwtrace.zip', 'artifacts-two-contexts-failing', - ' trace.zip', + ' trace.pwtrace.zip', ]); }); diff --git a/tests/playwright-test/playwright.reuse.spec.ts b/tests/playwright-test/playwright.reuse.spec.ts index 0620195f65..76286679b9 100644 --- a/tests/playwright-test/playwright.reuse.spec.ts +++ b/tests/playwright-test/playwright.reuse.spec.ts @@ -114,7 +114,7 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline expect(result.exitCode).toBe(0); expect(result.passed).toBe(2); - const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.zip')); + const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.pwtrace.zip')); expect(trace1.actionTree).toEqual([ 'Before Hooks', ' fixture: browser', @@ -131,7 +131,7 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline expect(trace1.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0); expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'trace-1.zip'))).toBe(false); - const trace2 = await parseTrace(testInfo.outputPath('test-results', 'reuse-two', 'trace.zip')); + const trace2 = await parseTrace(testInfo.outputPath('test-results', 'reuse-two', 'trace.pwtrace.zip')); expect(trace2.actionTree).toEqual([ 'Before Hooks', ' fixture: context', @@ -533,6 +533,6 @@ test('should survive serial mode with tracing and reuse', async ({ runInlineTest expect(result.exitCode).toBe(0); expect(result.passed).toBe(2); - expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'trace.zip'))).toBe(true); - expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'trace.zip'))).toBe(true); + expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'trace.pwtrace.zip'))).toBe(true); + expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'trace.pwtrace.zip'))).toBe(true); }); diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts index 5ca08925b4..a526eea44d 100644 --- a/tests/playwright-test/playwright.trace.spec.ts +++ b/tests/playwright-test/playwright.trace.spec.ts @@ -53,7 +53,7 @@ test('should stop tracing with trace: on-first-retry, when not retrying', async expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); expect(result.flaky).toBe(1); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-shared-flaky-retry1', 'trace.zip'))).toBeTruthy(); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-shared-flaky-retry1', 'trace.pwtrace.zip'))).toBeTruthy(); }); test('should record api trace', async ({ runInlineTest, server }, testInfo) => { @@ -86,7 +86,7 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => { expect(result.passed).toBe(2); expect(result.failed).toBe(1); // One trace file for request context and one for each APIRequestContext - const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip')); expect(trace1.actionTree).toEqual([ 'Before Hooks', ' fixture: request', @@ -105,14 +105,14 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => { ' fixture: request', ' apiRequestContext.dispose', ]); - const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-api-pass', 'trace.zip')); + const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-api-pass', 'trace.pwtrace.zip')); expect(trace2.actionTree).toEqual([ 'Before Hooks', 'apiRequest.newContext', 'apiRequestContext.get', 'After Hooks', ]); - const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip')); + const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.pwtrace.zip')); expect(trace3.actionTree).toEqual([ 'Before Hooks', ' fixture: request', @@ -204,7 +204,7 @@ test('should not mixup network files between contexts', async ({ runInlineTest, }, { workers: 1, timeout: 15000 }); expect(result.exitCode).toEqual(0); expect(result.passed).toBe(1); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-example', 'trace.zip'))).toBe(true); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-example', 'trace.pwtrace.zip'))).toBe(true); }); test('should save sources when requested', async ({ runInlineTest }, testInfo) => { @@ -224,7 +224,7 @@ test('should save sources when requested', async ({ runInlineTest }, testInfo) = `, }, { workers: 1 }); expect(result.exitCode).toEqual(0); - const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip')); expect([...resources.keys()].filter(name => name.startsWith('resources/src@'))).toHaveLength(1); }); @@ -248,7 +248,7 @@ test('should not save sources when not requested', async ({ runInlineTest }, tes `, }, { workers: 1 }); expect(result.exitCode).toEqual(0); - const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip')); expect([...resources.keys()].filter(name => name.startsWith('resources/src@'))).toHaveLength(0); }); @@ -283,8 +283,8 @@ test('should work in serial mode', async ({ runInlineTest }, testInfo) => { expect(result.exitCode).toBe(1); expect(result.passed).toBe(1); expect(result.failed).toBe(1); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-passes', 'trace.zip'))).toBeFalsy(); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-fails', 'trace.zip'))).toBeTruthy(); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-passes', 'trace.pwtrace.zip'))).toBeFalsy(); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-fails', 'trace.pwtrace.zip'))).toBeTruthy(); }); test('should not override trace file in afterAll', async ({ runInlineTest, server }, testInfo) => { @@ -313,7 +313,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve expect(result.exitCode).toBe(1); expect(result.passed).toBe(1); expect(result.failed).toBe(1); - const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip')); + const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-test-1', 'trace.pwtrace.zip')); expect(trace1.actionTree).toEqual([ 'Before Hooks', @@ -338,7 +338,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve ]); expect(trace1.errors).toEqual([`'oh no!'`]); - const error = await parseTrace(testInfo.outputPath('test-results', 'a-test-2', 'trace.zip')).catch(e => e); + const error = await parseTrace(testInfo.outputPath('test-results', 'a-test-2', 'trace.pwtrace.zip')).catch(e => e); expect(error).toBeTruthy(); }); @@ -366,8 +366,8 @@ test('should retain traces for interrupted tests', async ({ runInlineTest }, tes expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); expect(result.interrupted).toBe(1); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'))).toBeTruthy(); - expect(fs.existsSync(testInfo.outputPath('test-results', 'b-test-2', 'trace.zip'))).toBeTruthy(); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.pwtrace.zip'))).toBeTruthy(); + expect(fs.existsSync(testInfo.outputPath('test-results', 'b-test-2', 'trace.pwtrace.zip'))).toBeTruthy(); }); test('should respect --trace', async ({ runInlineTest }, testInfo) => { @@ -382,7 +382,7 @@ test('should respect --trace', async ({ runInlineTest }, testInfo) => { expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'))).toBeTruthy(); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.pwtrace.zip'))).toBeTruthy(); }); test('should respect PW_TEST_DISABLE_TRACING', async ({ runInlineTest }, testInfo) => { @@ -400,7 +400,7 @@ test('should respect PW_TEST_DISABLE_TRACING', async ({ runInlineTest }, testInf expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'))).toBe(false); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.pwtrace.zip'))).toBe(false); }); for (const mode of ['off', 'retain-on-failure', 'on-first-retry', 'on-all-retries', 'retain-on-first-failure']) { @@ -465,7 +465,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa }); `, }, { trace: 'retain-on-failure' }); - const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.apiNames).toContain('page.goto'); expect(result.failed).toBe(1); @@ -487,7 +487,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa }); `, }, { trace: 'retain-on-failure' }); - const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.apiNames).toContain('page.goto'); expect(result.failed).toBe(1); @@ -507,7 +507,7 @@ test(`trace:retain-on-failure should create trace if request context is disposed }); `, }, { trace: 'retain-on-failure' }); - const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.apiNames).toContain('apiRequestContext.get'); expect(result.failed).toBe(1); @@ -529,7 +529,7 @@ test('should include attachments by default', async ({ runInlineTest, server }, expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); - const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip')); expect(trace.apiNames).toEqual([ 'Before Hooks', `attach "foo"`, @@ -559,7 +559,7 @@ test('should opt out of attachments', async ({ runInlineTest, server }, testInfo expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); - const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip')); expect(trace.apiNames).toEqual([ 'Before Hooks', `attach "foo"`, @@ -592,7 +592,7 @@ test('should record with custom page fixture', async ({ runInlineTest }, testInf expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); expect(result.output).toContain('failure!'); - const trace = await parseTraceRaw(testInfo.outputPath('test-results', 'a-fails', 'trace.zip')); + const trace = await parseTraceRaw(testInfo.outputPath('test-results', 'a-fails', 'trace.pwtrace.zip')); expect(trace.events).toContainEqual(expect.objectContaining({ type: 'frame-snapshot', })); @@ -617,7 +617,7 @@ test('should expand expect.toPass', async ({ runInlineTest }, testInfo) => { expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); - const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip')); expect(trace.actionTree).toEqual([ 'Before Hooks', ' fixture: browser', @@ -656,7 +656,7 @@ test('should show non-expect error in trace', async ({ runInlineTest }, testInfo expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); - const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.pwtrace.zip')); expect(trace.actionTree).toEqual([ 'Before Hooks', ' fixture: browser', @@ -692,7 +692,7 @@ test('should show error from beforeAll in trace', async ({ runInlineTest }, test expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); - const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.pwtrace.zip')); expect(trace.errors).toEqual(['Error: Oh my!']); }); @@ -730,7 +730,7 @@ test('should not throw when attachment is missing', async ({ runInlineTest }, te expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); - const trace = await parseTrace(testInfo.outputPath('test-results', 'a-passes', 'trace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-passes', 'trace.pwtrace.zip')); expect(trace.actionTree).toContain('attach "screenshot"'); }); @@ -754,7 +754,7 @@ test('should not throw when screenshot on failure fails', async ({ runInlineTest expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); - const trace = await parseTrace(testInfo.outputPath('test-results', 'a-has-pdf-page', 'trace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-has-pdf-page', 'trace.pwtrace.zip')); const attachedScreenshots = trace.actionTree.filter(s => s.trim() === `attach "screenshot"`); // One screenshot for the page, no screenshot for pdf page since it should have failed. expect(attachedScreenshots.length).toBe(1); @@ -778,7 +778,7 @@ test('should use custom expect message in trace', async ({ runInlineTest }, test expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); - const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.pwtrace.zip')); expect(trace.actionTree).toEqual([ 'Before Hooks', ' fixture: browser', @@ -837,7 +837,7 @@ test('should not throw when merging traces multiple times', async ({ runInlineTe expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-foo', 'trace.zip'))).toBe(true); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-foo', 'trace.pwtrace.zip'))).toBe(true); }); test('should record nested steps, even after timeout', async ({ runInlineTest }, testInfo) => { @@ -928,7 +928,7 @@ test('should record nested steps, even after timeout', async ({ runInlineTest }, expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); - const trace = await parseTrace(testInfo.outputPath('test-results', 'a-example', 'trace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-example', 'trace.pwtrace.zip')); expect(trace.actionTree).toEqual([ 'Before Hooks', ' beforeAll hook', @@ -1022,14 +1022,14 @@ test('should attribute worker fixture teardown to the right test', async ({ runI expect(result.exitCode).toBe(1); expect(result.passed).toBe(1); expect(result.failed).toBe(1); - const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-one', 'trace.zip')); + const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-one', 'trace.pwtrace.zip')); expect(trace1.actionTree).toEqual([ 'Before Hooks', ' fixture: foo', ' step in foo setup', 'After Hooks', ]); - const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-two', 'trace.zip')); + const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-two', 'trace.pwtrace.zip')); expect(trace2.actionTree).toEqual([ 'Before Hooks', 'After Hooks', @@ -1050,11 +1050,11 @@ test('trace:retain-on-first-failure should create trace but only on first failur `, }, { trace: 'retain-on-first-failure', retries: 1 }); - const retryTracePath = test.info().outputPath('test-results', 'a-fail-retry1', 'trace.zip'); + const retryTracePath = test.info().outputPath('test-results', 'a-fail-retry1', 'trace.pwtrace.zip'); const retryTraceExists = fs.existsSync(retryTracePath); expect(retryTraceExists).toBe(false); - const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.apiNames).toContain('page.goto'); expect(result.failed).toBe(1); @@ -1071,7 +1071,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef }); `, }, { trace: 'retain-on-first-failure' }); - const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.apiNames).toContain('page.goto'); expect(result.failed).toBe(1); @@ -1090,7 +1090,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef }); `, }, { trace: 'retain-on-first-failure' }); - const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.apiNames).toContain('page.goto'); expect(result.failed).toBe(1); @@ -1107,7 +1107,7 @@ test('trace:retain-on-first-failure should create trace if request context is di }); `, }, { trace: 'retain-on-first-failure' }); - const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.apiNames).toContain('apiRequestContext.get'); expect(result.failed).toBe(1); @@ -1132,7 +1132,7 @@ test('should not corrupt actions when no library trace is present', async ({ run expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); - const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.actionTree).toEqual([ 'Before Hooks', @@ -1162,7 +1162,7 @@ test('should record trace for manually created context in a failed test', async expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); - const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.actionTree).toEqual([ 'Before Hooks', @@ -1204,7 +1204,7 @@ test('should not nest top level expect into unfinished api calls ', { expect(result.exitCode).toBe(0); expect(result.failed).toBe(0); - const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.actionTree).toEqual([ 'Before Hooks', @@ -1246,7 +1246,7 @@ test('should record trace after fixture teardown timeout', { expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); - const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.pwtrace.zip'); const trace = await parseTrace(tracePath); expect(trace.actionTree).toEqual([ 'Before Hooks', diff --git a/tests/playwright-test/reporter-attachment.spec.ts b/tests/playwright-test/reporter-attachment.spec.ts index 50d6d1ee0c..99345a9107 100644 --- a/tests/playwright-test/reporter-attachment.spec.ts +++ b/tests/playwright-test/reporter-attachment.spec.ts @@ -66,7 +66,7 @@ test('render trace attachment', async ({ runInlineTest }) => { test('one', async ({}, testInfo) => { testInfo.attachments.push({ name: 'trace', - path: testInfo.outputPath('my dir with space', 'trace.zip'), + path: testInfo.outputPath('my dir with space', 'trace.pwtrace.zip'), contentType: 'application/zip' }); expect(1).toBe(0); @@ -75,8 +75,8 @@ test('render trace attachment', async ({ runInlineTest }) => { }, { reporter: 'line' }); const text = result.output.replace(/\\/g, '/'); expect(text).toContain(' attachment #1: trace (application/zip) ─────────────────────────────────────────────────────────'); - expect(text).toContain(' test-results/a-one/my dir with space/trace.zip'); - expect(text).toContain('npx playwright show-trace "test-results/a-one/my dir with space/trace.zip"'); + expect(text).toContain(' test-results/a-one/my dir with space/trace.pwtrace.zip'); + expect(text).toContain('npx playwright show-trace "test-results/a-one/my dir with space/trace.pwtrace.zip"'); expect(text).toContain(' ────────────────────────────────────────────────────────────────────────────────────────────────'); expect(result.exitCode).toBe(1); }); From c24ad36f8639283d583cd38ba74f65a9905e8737 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 16 Sep 2024 07:39:36 +0200 Subject: [PATCH 11/12] docs(docker): fix Docker container permissions (#32621) --- docs/src/ci.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/src/ci.md b/docs/src/ci.md index 97ea4ad8d2..745ecc9d50 100644 --- a/docs/src/ci.md +++ b/docs/src/ci.md @@ -209,6 +209,7 @@ jobs: runs-on: ubuntu-latest container: image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy + options: --user 1001 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -218,8 +219,6 @@ jobs: run: npm ci - name: Run your tests run: npx playwright test - env: - HOME: /root ``` ```yml python title=".github/workflows/playwright.yml" @@ -235,6 +234,7 @@ jobs: runs-on: ubuntu-latest container: image: mcr.microsoft.com/playwright/python:v%%VERSION%%-jammy + options: --user 1001 steps: - uses: actions/checkout@v4 - name: Set up Python @@ -248,8 +248,6 @@ jobs: pip install -e . - name: Run your tests run: pytest - env: - HOME: /root ``` ```yml java title=".github/workflows/playwright.yml" @@ -265,6 +263,7 @@ jobs: runs-on: ubuntu-latest container: image: mcr.microsoft.com/playwright/java:v%%VERSION%%-jammy + options: --user 1001 steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 @@ -275,8 +274,6 @@ jobs: run: mvn -B install -D skipTests --no-transfer-progress - name: Run tests run: mvn test - env: - HOME: /root ``` ```yml csharp title=".github/workflows/playwright.yml" @@ -292,6 +289,7 @@ jobs: runs-on: ubuntu-latest container: image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-jammy + options: --user 1001 steps: - uses: actions/checkout@v4 - name: Setup dotnet @@ -301,8 +299,6 @@ jobs: - run: dotnet build - name: Run your tests run: dotnet test - env: - HOME: /root ``` #### On deployment From 268357238ac27e03ea5309ab23e70ba9261e68f1 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 16 Sep 2024 00:10:06 -0700 Subject: [PATCH 12/12] fix(expect): respect custom message in expect.poll (#32603) Fixes #32582. --- packages/playwright/src/matchers/expect.ts | 48 +++++++++++++--------- tests/playwright-test/expect-poll.spec.ts | 18 +++++++- tests/playwright-test/test-step.spec.ts | 6 +++ 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 9c426fd86e..3a254ae7bf 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -121,10 +121,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re const [actual, messageOrOptions] = argumentsList; const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message; const newInfo = { ...info, message }; - if (newInfo.isPoll) { + if (newInfo.poll) { if (typeof actual !== 'function') throw new Error('`expect.poll()` accepts only function as a first argument'); - newInfo.generator = actual as any; + newInfo.poll.generator = actual as any; } return createMatchers(actual, newInfo, prefix); }, @@ -189,10 +189,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re if ('soft' in configuration) newInfo.isSoft = configuration.soft; if ('_poll' in configuration) { - newInfo.isPoll = !!configuration._poll; + newInfo.poll = configuration._poll ? { ...info.poll, generator: () => {} } : undefined; if (typeof configuration._poll === 'object') { - newInfo.pollTimeout = configuration._poll.timeout; - newInfo.pollIntervals = configuration._poll.intervals; + newInfo.poll!.timeout = configuration._poll.timeout ?? newInfo.poll!.timeout; + newInfo.poll!.intervals = configuration._poll.intervals ?? newInfo.poll!.intervals; } } return createExpect(newInfo, prefix, customMatchers); @@ -249,11 +249,12 @@ type ExpectMetaInfo = { message?: string; isNot?: boolean; isSoft?: boolean; - isPoll?: boolean; + poll?: { + timeout?: number; + intervals?: number[]; + generator: Generator; + }; timeout?: number; - pollTimeout?: number; - pollIntervals?: number[]; - generator?: Generator; }; class ExpectMetaInfoProxyHandler implements ProxyHandler { @@ -287,10 +288,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { this._info.isNot = !this._info.isNot; return new Proxy(matcher, this); } - if (this._info.isPoll) { + if (this._info.poll) { if ((customAsyncMatchers as any)[matcherName] || matcherName === 'resolves' || matcherName === 'rejects') throw new Error(`\`expect.poll()\` does not support "${matcherName}" matcher.`); - matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, !!this._info.isNot, this._info.pollIntervals, this._info.pollTimeout ?? currentExpectTimeout(), this._info.generator!, ...args); + matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, this._info, this._prefix, ...args); } return (...args: any[]) => { const testInfo = currentTestInfo(); @@ -302,7 +303,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { const customMessage = this._info.message || ''; const argsSuffix = computeArgsSuffix(matcherName, args); - const defaultTitle = `expect${this._info.isPoll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`; + const defaultTitle = `expect${this._info.poll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`; const title = customMessage || defaultTitle; // This looks like it is unnecessary, but it isn't - we need to filter @@ -336,7 +337,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { const callback = () => matcher.call(target, ...args); // toPass and poll matchers can contain other steps, expects and API calls, // so they behave like a retriable step. - const result = (matcherName === 'toPass' || this._info.isPoll) ? + const result = (matcherName === 'toPass' || this._info.poll) ? zones.run('stepZone', step, callback) : zones.run('expectZone', { title, stepId: step.stepId }, callback); if (result instanceof Promise) @@ -350,25 +351,32 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { } } -async function pollMatcher(qualifiedMatcherName: any, isNot: boolean, pollIntervals: number[] | undefined, timeout: number, generator: () => any, ...args: any[]) { +async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) { const testInfo = currentTestInfo(); + const poll = info.poll!; + const timeout = poll.timeout ?? currentExpectTimeout(); const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout); const result = await pollAgainstDeadline(async () => { if (testInfo && currentTestInfo() !== testInfo) return { continuePolling: false, result: undefined }; - const value = await generator(); - let expectInstance = expectLibrary(value) as any; - if (isNot) - expectInstance = expectInstance.not; + const innerInfo: ExpectMetaInfo = { + ...info, + isSoft: false, // soft is outside of poll, not inside + poll: undefined, + }; + const value = await poll.generator(); try { - expectInstance[qualifiedMatcherName].call(expectInstance, ...args); + let matchers = createMatchers(value, innerInfo, prefix); + if (info.isNot) + matchers = matchers.not; + matchers[qualifiedMatcherName](...args); return { continuePolling: false, result: undefined }; } catch (error) { return { continuePolling: true, result: error }; } - }, deadline, pollIntervals ?? [100, 250, 500, 1000]); + }, deadline, poll.intervals ?? [100, 250, 500, 1000]); if (result.timedOut) { const message = result.result ? [ diff --git a/tests/playwright-test/expect-poll.spec.ts b/tests/playwright-test/expect-poll.spec.ts index 344fdccdee..208ac4ccce 100644 --- a/tests/playwright-test/expect-poll.spec.ts +++ b/tests/playwright-test/expect-poll.spec.ts @@ -262,4 +262,20 @@ test('should propagate string exception from async arrow function', { annotation }); expect(result.output).toContain('some error'); -}); \ No newline at end of file +}); + +test('should show custom message', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32582' } +}, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('should fail', async () => { + await expect.poll(() => 1, { message: 'custom message', timeout: 500 }).toBe(2); + }); + `, + }); + expect(result.output).toContain('Error: custom message'); + expect(result.output).toContain('Expected: 2'); + expect(result.output).toContain('Received: 1'); +}); diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index 96539a4c88..d14bccd98b 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -987,9 +987,12 @@ expect |expect.poll.toHaveLength @ a.test.ts:14 pw:api | page.goto(about:blank) @ a.test.ts:7 test.step | inner step attempt: 0 @ a.test.ts:8 expect | expect.toBe @ a.test.ts:10 +expect | expect.toHaveLength @ a.test.ts:6 +expect | ↪ error: Error: expect(received).toHaveLength(expected) pw:api | page.goto(about:blank) @ a.test.ts:7 test.step | inner step attempt: 1 @ a.test.ts:8 expect | expect.toBe @ a.test.ts:10 +expect | expect.toHaveLength @ a.test.ts:6 hook |After Hooks fixture | fixture: page fixture | fixture: context @@ -1036,9 +1039,12 @@ expect |expect.poll.toBe @ a.test.ts:13 expect | expect.toHaveText @ a.test.ts:7 test.step | iteration 1 @ a.test.ts:9 expect | expect.toBeVisible @ a.test.ts:10 +expect | expect.toBe @ a.test.ts:6 +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | expect.toHaveText @ a.test.ts:7 test.step | iteration 2 @ a.test.ts:9 expect | expect.toBeVisible @ a.test.ts:10 +expect | expect.toBe @ a.test.ts:6 hook |After Hooks fixture | fixture: page fixture | fixture: context