From b23edf5137c02fb81d8a66ef719034dabd0828c8 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Tue, 17 Sep 2024 09:36:43 +0100 Subject: [PATCH 1/6] fix(docs): remove todo in ci intro docs (#32643) --- docs/src/ci-intro.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/ci-intro.md b/docs/src/ci-intro.md index 492130088a..9e0ee0eb4f 100644 --- a/docs/src/ci-intro.md +++ b/docs/src/ci-intro.md @@ -25,7 +25,7 @@ Playwright tests can be ran on any CI provider. In this section we will cover ru #### You will learn * langs: python, java, csharp - + - [How to set up GitHub Actions](/ci-intro.md#setting-up-github-actions) - [How to view test logs](/ci-intro.md#viewing-test-logs) - [How to view the trace](/ci-intro.md#viewing-the-trace) @@ -322,5 +322,5 @@ This step will not work for pull requests created from a forked repository becau - [Learn how to perform Actions](./input.md) - [Learn how to write Assertions](./test-assertions.md) - [Learn more about the Trace Viewer](/trace-viewer.md) -- [Learn more ways of running tests on GitHub Actions](/ci.md) -- [Learn more about running tests on other CI providers](/ci.md#github-actions) // TODO: is this link correct? \ No newline at end of file +- [Learn more ways of running tests on GitHub Actions](/ci.md#github-actions) +- [Learn more about running tests on other CI providers](/ci.md) From f6219e6e793054a89e89868f2277f194586086ea Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 17 Sep 2024 15:32:30 +0200 Subject: [PATCH 2/6] Revert "feat(tracing): add .pwtrace to trace file extension" (#32648) Reverts microsoft/playwright#32581 Relates https://github.com/microsoft/playwright/issues/32226#issuecomment-2351164727 --- 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 f35e9e53ff..6e7541e4cb 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.pwtrace.zip' }); +await context.tracing.stop({ path: 'trace.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.pwtrace.zip"))); + .setPath(Paths.get("trace.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.pwtrace.zip") +await context.tracing.stop(path = "trace.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.pwtrace.zip") +context.tracing.stop(path = "trace.zip") ``` ```csharp @@ -57,7 +57,7 @@ var page = await context.NewPageAsync(); await page.GotoAsync("https://playwright.dev"); await context.Tracing.StopAsync(new() { - Path = "trace.pwtrace.zip" + Path = "trace.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.pwtrace.zip' }); +await context.tracing.stop({ path: 'trace.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.pwtrace.zip"))); + .setPath(Paths.get("trace.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.pwtrace.zip") +await context.tracing.stop(path = "trace.zip") ``` ```python sync context.tracing.start(screenshots=True, snapshots=True) page = context.new_page() page.goto("https://playwright.dev") -context.tracing.stop(path = "trace.pwtrace.zip") +context.tracing.stop(path = "trace.zip") ``` ```csharp @@ -112,7 +112,7 @@ var page = await context.NewPageAsync(); await page.GotoAsync("https://playwright.dev"); await context.Tracing.StopAsync(new() { - Path = "trace.pwtrace.zip" + Path = "trace.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.pwtrace.zip' }); +await context.tracing.stopChunk({ path: 'trace1.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.pwtrace.zip' }); +await context.tracing.stopChunk({ path: 'trace2.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.pwtrace.zip"))); + .setPath(Paths.get("trace1.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.pwtrace.zip"))); + .setPath(Paths.get("trace2.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.pwtrace.zip") +await context.tracing.stop_chunk(path = "trace1.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.pwtrace.zip") +await context.tracing.stop_chunk(path = "trace2.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.pwtrace.zip") +context.tracing.stop_chunk(path = "trace1.zip") context.tracing.start_chunk() page.goto("http://example.com") # Save a second trace file with different actions. -context.tracing.stop_chunk(path = "trace2.pwtrace.zip") +context.tracing.stop_chunk(path = "trace2.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.pwtrace.zip" + Path = "trace1.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.pwtrace.zip" + Path = "trace2.zip" }); ``` diff --git a/docs/src/test-global-setup-teardown-js.md b/docs/src/test-global-setup-teardown-js.md index 0bb05cd78d..883bdf25d6 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.pwtrace.zip', + path: './test-results/setup-trace.zip', }); await browser.close(); } catch (error) { await context.tracing.stop({ - path: './test-results/failed-setup-trace.pwtrace.zip', + path: './test-results/failed-setup-trace.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 db4f096b9e..79a415e54a 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.pwtrace.zip` in your `test-results` directory. +This will record the trace and place it into the file named `trace.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.pwtrace.zip") +await context.tracing.stop(path = "trace.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.pwtrace.zip") +context.tracing.stop(path = "trace.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.pwtrace.zip"))); + .setPath(Paths.get("trace.zip"))); ``` -This will record the trace and place it into the file named `trace.pwtrace.zip`. +This will record the trace and place it into the file named `trace.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.pwtrace.zip" +mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip" ``` ```bash python -playwright show-trace trace.pwtrace.zip +playwright show-trace trace.zip ``` ###### diff --git a/docs/src/trace-viewer-intro-js.md b/docs/src/trace-viewer-intro-js.md index b7ac93dd9c..a950689468 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.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. +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. ```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 ad26278930..207646c1d7 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.pwtrace.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.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.pwtrace.zip' }); +await context.tracing.stop({ path: 'trace.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.pwtrace.zip` in your `test-results` directory. +This will record the trace and place it into the file named `trace.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.pwtrace.zip") +await context.tracing.stop(path = "trace.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.pwtrace.zip") +context.tracing.stop(path = "trace.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.pwtracezip"))); + .setPath(Paths.get("trace.zip"))); ``` -This will record the trace and place it into the file named `trace.pwtrace.zip`. +This will record the trace and place it into the file named `trace.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.pwtrace.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.zip` file is located. ```bash js -npx playwright show-trace path/to/trace.pwtrace.zip +npx playwright show-trace path/to/trace.zip ``` ```bash java -mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.pwtrace.zip" +mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip" ``` ```bash python -playwright show-trace trace.pwtrace.zip +playwright show-trace trace.zip ``` ```bash csharp -pwsh bin/Debug/netX/playwright.ps1 show-trace trace.pwtrace.zip +pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip ``` ## Using [trace.playwright.dev](https://trace.playwright.dev) @@ -496,19 +496,19 @@ pwsh bin/Debug/netX/playwright.ps1 show-trace trace.pwtrace.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.pwtrace.zip +npx playwright show-trace https://example.com/trace.zip ``` ```bash java -mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace https://example.com/trace.pwtrace.zip" +mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace https://example.com/trace.zip" ``` ```bash python -playwright show-trace https://example.com/trace.pwtrace.zip +playwright show-trace https://example.com/trace.zip ``` ```bash csharp -pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.pwtrace.zip +pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.zip ``` diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 6a36210f8e..d8fa8230c6 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.pwtrace.zip`); + $ show-trace https://example.com/trace.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 13f15b6222..b09bbe3134 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 + '.pwtrace.zip'; + const zipFileName = this._state.traceFile + '.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 f770fc70dc..ad102f9271 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -19927,7 +19927,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.pwtrace.zip' }); + * await context.tracing.stop({ path: 'trace.zip' }); * ``` * */ @@ -19941,7 +19941,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.pwtrace.zip' }); + * await context.tracing.stop({ path: 'trace.zip' }); * ``` * * @param options @@ -19996,12 +19996,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.pwtrace.zip' }); + * await context.tracing.stopChunk({ path: 'trace1.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.pwtrace.zip' }); + * await context.tracing.stopChunk({ path: 'trace2.zip' }); * ``` * * @param options diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index 1b58c5ebf6..fed7fdde7e 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() + '.pwtrace.zip'); + const file = path.join(this._artifactsDir, createGuid() + '.zip'); this._temporaryTraceFiles.push(file); return file; } @@ -214,7 +214,7 @@ export class TestTracing { }); }); - const tracePath = this._testInfo.outputPath('trace.pwtrace.zip'); + const tracePath = this._testInfo.outputPath('trace.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 936c8ea998..3b79a55e98 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.pwtrace.zip'); + const traceFile = testInfo.outputPath('trace.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 32b529224f..5c6ced0948 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.pwtrace.zip'); + const path = test.info().outputPath('electron-trace.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.pwtrace.zip'), + path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'electron-trace.zip'), // contains the expect() calls - path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'trace.pwtrace.zip'), + path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'trace.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 4f7c3e611c..14590a3ae3 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.pwtrace.zip') }).catch(e => e); + const error = await context.tracing.stopChunk({ path: testInfo.outputPath('trace.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 55070f3ec8..a473539e8e 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.pwtrace.zip'); + const traceZip = testInfo.outputPath('trace.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 610d87fc05..ef67cd8b93 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.pwtrace.zip'); + const traceFileName = testInfo.outputPath('trace.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.pwtrace.zip'); + const traceFileName = testInfo.outputPath('trace.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 0c6ef86f25..2b00949b19 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.pwtrace.zip'); + traceFile = path.join(workerInfo.project.outputDir, String(workerInfo.workerIndex), browserName, 'trace.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.pwtrace.zip'); + const traceFile = test.info().outputPath('trace.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.pwtrace.zip'); + const traceFile = test.info().outputPath('trace.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 }) => { - const traceFile = test.info().outputPath('trace.pwtrace.zip'); +test('should remove noscript by default', async ({ browser, server, showTraceViewer, browserType }) => { + const traceFile = test.info().outputPath('trace.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 }) => { - const traceFile = test.info().outputPath('trace.pwtrace.zip'); +test('should remove noscript when javaScriptEnabled is set to true', async ({ browser, server, showTraceViewer, browserType }) => { + const traceFile = test.info().outputPath('trace.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 770ea0362c..c82db377aa 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.pwtrace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); - const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); + const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip') }); - const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); + await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); + const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); - const { events } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); + const { events } = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip') }); - const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); + await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); + const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); - const trace = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); + const trace = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip') }); - const { events } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); + await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); + const { events } = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); - const { events, resources } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); + const { events, resources } = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip') }); + await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); await context.close(); - const { events } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); + const { events } = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip') }).catch(e => e); + const error = await context.tracing.stop({ path: testInfo.outputPath('trace.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.pwtrace.zip') }); + await context.tracing.stopChunk({ path: testInfo.outputPath('trace.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.pwtrace.zip')); + const trace1 = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip') }), + context.tracing.stop({ path: testInfo.outputPath('trace.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.pwtrace.zip') }); + await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') }); - const trace = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip')); + const trace = await parseTraceRaw(testInfo.outputPath('trace.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.pwtrace.zip'); + const tracePath = testInfo.outputPath('trace.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.pwtrace.zip'); + const tracePath = testInfo.outputPath('trace.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.pwtrace.zip'); + const tracePath = testInfo.outputPath('trace.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.pwtrace.zip'); + const tracePath = testInfo.outputPath('trace.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.pwtrace.zip'); + const tracePath = testInfo.outputPath('trace.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.pwtrace.zip'); + const tracePath = testInfo.outputPath('trace.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 1413ba1ed5..39dcaecbc6 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.pwtrace.zip'); + const traceFile = testInfo.outputPath('trace.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 cc86c6c584..b666aa4b70 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.pwtrace.zip', + ' trace.zip', 'artifacts-own-context-failing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-own-context-passing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-passing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-persistent-failing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-persistent-passing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-shared-shared-failing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-shared-shared-passing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-two-contexts', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-two-contexts-failing', - ' trace.pwtrace.zip', + ' trace.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.pwtrace.zip', + ' trace.zip', 'artifacts-own-context-failing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-persistent-failing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-shared-shared-failing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-two-contexts-failing', - ' trace.pwtrace.zip', + ' trace.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.pwtrace.zip', + ' trace.zip', 'artifacts-own-context-failing-retry1', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-persistent-failing-retry1', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-shared-shared-failing-retry1', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-two-contexts-failing-retry1', - ' trace.pwtrace.zip', + ' trace.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.pwtrace.zip', + ' trace.zip', 'artifacts-failing-retry2', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-own-context-failing-retry1', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-own-context-failing-retry2', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-persistent-failing-retry1', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-persistent-failing-retry2', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-shared-shared-failing-retry1', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-shared-shared-failing-retry2', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-two-contexts-failing-retry1', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-two-contexts-failing-retry2', - ' trace.pwtrace.zip', + ' trace.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.pwtrace.zip', + ' trace.zip', 'artifacts-own-context-failing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-persistent-failing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-shared-shared-failing', - ' trace.pwtrace.zip', + ' trace.zip', 'artifacts-two-contexts-failing', - ' trace.pwtrace.zip', + ' trace.zip', ]); }); diff --git a/tests/playwright-test/playwright.reuse.spec.ts b/tests/playwright-test/playwright.reuse.spec.ts index 76286679b9..0620195f65 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.pwtrace.zip')); + const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.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.pwtrace.zip')); + const trace2 = await parseTrace(testInfo.outputPath('test-results', 'reuse-two', 'trace.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.pwtrace.zip'))).toBe(true); - expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'trace.pwtrace.zip'))).toBe(true); + 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); }); diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts index a526eea44d..5ca08925b4 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.pwtrace.zip'))).toBeTruthy(); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-shared-flaky-retry1', 'trace.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.pwtrace.zip')); + const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.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.pwtrace.zip')); + const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-api-pass', 'trace.zip')); expect(trace2.actionTree).toEqual([ 'Before Hooks', 'apiRequest.newContext', 'apiRequestContext.get', 'After Hooks', ]); - const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.pwtrace.zip')); + const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip'))).toBe(true); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-example', 'trace.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.pwtrace.zip')); + const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.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.pwtrace.zip')); + const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.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.pwtrace.zip'))).toBeFalsy(); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-fails', 'trace.pwtrace.zip'))).toBeTruthy(); + 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(); }); 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.pwtrace.zip')); + const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-test-1', 'trace.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.pwtrace.zip')).catch(e => e); + const error = await parseTrace(testInfo.outputPath('test-results', 'a-test-2', 'trace.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.pwtrace.zip'))).toBeTruthy(); - expect(fs.existsSync(testInfo.outputPath('test-results', 'b-test-2', 'trace.pwtrace.zip'))).toBeTruthy(); + 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(); }); 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.pwtrace.zip'))).toBeTruthy(); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.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.pwtrace.zip'))).toBe(false); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.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.pwtrace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.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.pwtrace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.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.pwtrace.zip')); + const trace = await parseTraceRaw(testInfo.outputPath('test-results', 'a-fails', 'trace.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.pwtrace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.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.pwtrace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-passes', 'trace.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.pwtrace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-has-pdf-page', 'trace.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.pwtrace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip'))).toBe(true); + expect(fs.existsSync(testInfo.outputPath('test-results', 'a-foo', 'trace.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.pwtrace.zip')); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-example', 'trace.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.pwtrace.zip')); + const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-one', 'trace.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.pwtrace.zip')); + const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-two', 'trace.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.pwtrace.zip'); + const retryTracePath = test.info().outputPath('test-results', 'a-fail-retry1', 'trace.zip'); const retryTraceExists = fs.existsSync(retryTracePath); expect(retryTraceExists).toBe(false); - const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.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.pwtrace.zip'); + const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.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 99345a9107..50d6d1ee0c 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.pwtrace.zip'), + path: testInfo.outputPath('my dir with space', 'trace.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.pwtrace.zip'); - expect(text).toContain('npx playwright show-trace "test-results/a-one/my dir with space/trace.pwtrace.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(' ────────────────────────────────────────────────────────────────────────────────────────────────'); expect(result.exitCode).toBe(1); }); From c216c25a1d325b7dc5f91438481fa6e2f10c160e Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 17 Sep 2024 15:54:22 +0200 Subject: [PATCH 3/6] feat(html-reporter): add file name copy button (#32652) --- packages/html-reporter/src/testCaseView.css | 1 + packages/html-reporter/src/testCaseView.spec.tsx | 6 +++--- packages/html-reporter/src/testCaseView.tsx | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/html-reporter/src/testCaseView.css b/packages/html-reporter/src/testCaseView.css index d87cf3aacb..90e2f4057b 100644 --- a/packages/html-reporter/src/testCaseView.css +++ b/packages/html-reporter/src/testCaseView.css @@ -47,6 +47,7 @@ flex: none; align-items: center; padding: 0 8px 8px; + line-height: 24px; } .test-case-path { diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index 72552f5184..7c9c99eeb3 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -81,9 +81,9 @@ test('should render copy buttons for annotations', async ({ mount, page, context const component = await mount(); await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible(); - component.getByText('Annotation text', { exact: false }).first().hover(); - await expect(component.getByLabel('Copy to clipboard').first()).toBeVisible(); - await component.getByLabel('Copy to clipboard').first().click(); + await component.getByText('Annotation text', { exact: false }).first().hover(); + await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible(); + await component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first().click(); const handle = await page.evaluateHandle(() => navigator.clipboard.readText()); const clipboardContent = await handle.jsonValue(); expect(clipboardContent).toBe('Annotation text'); diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index 1fe4f42ed3..e3656bf414 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -50,7 +50,11 @@ export const TestCaseView: React.FC<{ {test &&
{test.path.join(' › ')}
} {test &&
{test?.title}
} {test &&
-
{test.location.file}:{test.location.line}
+
+ + {test.location.file}:{test.location.line} + +
{msToString(test.duration)}
} From 751b939d3afe7021d8ebf73dd44b66a177f95698 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 17 Sep 2024 16:11:21 +0200 Subject: [PATCH 4/6] feat(fetch): record timings (#32613) Related to https://github.com/microsoft/playwright/issues/19621 Adds some instrumentation to collect timings for `APIRequestContext` requests and adds them to the HAR trace. Doesn't yet expose them via an API, but makes our `Duration` field in the trace viewer show a nice duration: Screenshot 2024-09-14 at 11 46 04 I'm gonna add it to our API in a separate PR. --------- Signed-off-by: Simon Knott Co-authored-by: Dmitry Gozman --- packages/playwright-core/src/server/fetch.ts | 40 ++++++++++++++++- .../src/server/har/harTracer.ts | 6 +++ .../src/utils/happy-eyeballs.ts | 15 +++++++ tests/library/har.spec.ts | 45 ++++++++++++++++++- 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index fc4e2c027d..2aa7f27fee 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -30,7 +30,7 @@ import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle'; import { BrowserContext, verifyClientCertificates } from './browserContext'; import { CookieStore, domainMatches, parseRawCookie } from './cookieStore'; import { MultipartFormData } from './formData'; -import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs'; +import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent, timingForSocket } from '../utils/happy-eyeballs'; import type { CallMetadata } from './instrumentation'; import { SdkObject } from './instrumentation'; import type { Playwright } from './playwright'; @@ -40,6 +40,7 @@ import { Tracing } from './trace/recorder/tracing'; import type * as types from './types'; import type { HeadersArray, ProxySettings } from './types'; import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; +import type * as har from '@trace/har'; type FetchRequestOptions = { userAgent: string; @@ -71,6 +72,7 @@ export type APIRequestFinishedEvent = { statusCode: number; statusMessage: string; body?: Buffer; + timings: har.Timings; }; type SendRequestOptions = https.RequestOptions & { @@ -294,8 +296,28 @@ export abstract class APIRequestContext extends SdkObject { // If we have a proxy agent already, do not override it. const agent = options.agent || (url.protocol === 'https:' ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent); const requestOptions = { ...options, agent }; + + const startAt = monotonicTime(); + let dnsLookupAt: number | undefined; + let tcpConnectionAt: number | undefined; + let tlsHandshakeAt: number | undefined; + let requestFinishAt: number | undefined; + const request = requestConstructor(url, requestOptions as any, async response => { + const responseAt = monotonicTime(); const notifyRequestFinished = (body?: Buffer) => { + const endAt = monotonicTime(); + // spec: http://www.softwareishard.com/blog/har-12-spec/#timings + const timings: har.Timings = { + send: requestFinishAt! - startAt, + wait: responseAt - requestFinishAt!, + receive: endAt - responseAt, + dns: dnsLookupAt ? dnsLookupAt - startAt : -1, + connect: (tlsHandshakeAt ?? tcpConnectionAt!) - startAt, // "If [ssl] is defined then the time is also included in the connect field " + ssl: tlsHandshakeAt ? tlsHandshakeAt - tcpConnectionAt! : -1, + blocked: -1, + }; + const requestFinishedEvent: APIRequestFinishedEvent = { requestEvent, httpVersion: response.httpVersion, @@ -304,7 +326,8 @@ export abstract class APIRequestContext extends SdkObject { headers: response.headers, rawHeaders: response.rawHeaders, cookies, - body + body, + timings, }; this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent); }; @@ -450,6 +473,19 @@ export abstract class APIRequestContext extends SdkObject { this.on(APIRequestContext.Events.Dispose, disposeListener); request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener)); + request.on('socket', socket => { + // happy eyeballs don't emit lookup and connect events, so we use our custom ones + const happyEyeBallsTimings = timingForSocket(socket); + dnsLookupAt = happyEyeBallsTimings.dnsLookupAt; + tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt; + + // non-happy-eyeballs sockets + socket.on('lookup', () => { dnsLookupAt = monotonicTime(); }); + socket.on('connect', () => { tcpConnectionAt = monotonicTime(); }); + socket.on('secureConnect', () => { tlsHandshakeAt = monotonicTime(); }); + }); + request.on('finish', () => { requestFinishAt = monotonicTime(); }); + progress.log(`→ ${options.method} ${url.toString()}`); if (options.headers) { for (const [name, value] of Object.entries(options.headers)) diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index e6330f3889..76da6682d4 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -212,6 +212,12 @@ export class HarTracer { harEntry.response.statusText = event.statusMessage; harEntry.response.httpVersion = event.httpVersion; harEntry.response.redirectURL = event.headers.location || ''; + + if (!this._options.omitTiming) { + harEntry.timings = event.timings; + this._computeHarEntryTotalTime(harEntry); + } + for (let i = 0; i < event.rawHeaders.length; i += 2) { harEntry.response.headers.push({ name: event.rawHeaders[i], diff --git a/packages/playwright-core/src/utils/happy-eyeballs.ts b/packages/playwright-core/src/utils/happy-eyeballs.ts index 12f082e5a1..18de1a938c 100644 --- a/packages/playwright-core/src/utils/happy-eyeballs.ts +++ b/packages/playwright-core/src/utils/happy-eyeballs.ts @@ -21,6 +21,7 @@ import * as net from 'net'; import * as tls from 'tls'; import { ManualPromise } from './manualPromise'; import { assert } from './debug'; +import { monotonicTime } from './time'; // Implementation(partial) of Happy Eyeballs 2 algorithm described in // https://www.rfc-editor.org/rfc/rfc8305 @@ -28,6 +29,9 @@ import { assert } from './debug'; // Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102) const connectionAttemptDelayMs = 300; +const kDNSLookupAt = Symbol('kDNSLookupAt') +const kTCPConnectionAt = Symbol('kTCPConnectionAt') + class HttpHappyEyeballsAgent extends http.Agent { createConnection(options: http.ClientRequestArgs, oncreate?: (err: Error | null, socket?: net.Socket) => void): net.Socket | undefined { // There is no ambiguity in case of IP address. @@ -107,6 +111,7 @@ export async function createConnectionAsync( const lookup = (options as any).__testHookLookup || lookupAddresses; const hostname = clientRequestArgsToHostName(options); const addresses = await lookup(hostname); + const dnsLookupAt = monotonicTime(); const sockets = new Set(); let firstError; let errorCount = 0; @@ -132,9 +137,13 @@ export async function createConnectionAsync( port: options.port as number, host: address }); + (socket as any)[kDNSLookupAt] = dnsLookupAt; + // Each socket may fire only one of 'connect', 'timeout' or 'error' events. // None of these events are fired after socket.destroy() is called. socket.on('connect', () => { + (socket as any)[kTCPConnectionAt] = monotonicTime(); + connected.resolve(); oncreate?.(null, socket); // TODO: Cache the result? @@ -189,3 +198,9 @@ function clientRequestArgsToHostName(options: http.ClientRequestArgs): string { throw new Error('Either options.hostname or options.host must be provided'); } +export function timingForSocket(socket: net.Socket | tls.TLSSocket) { + return { + dnsLookupAt: (socket as any)[kDNSLookupAt] as number | undefined, + tcpConnectionAt: (socket as any)[kTCPConnectionAt] as number | undefined, + } +} diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index 29ee7df2d1..cc65127e3f 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -24,9 +24,9 @@ import type { Log } from '../../packages/trace/src/har'; import { parseHar } from '../config/utils'; const { createHttp2Server } = require('../../packages/playwright-core/lib/utils'); -async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise, testInfo: any, options: { outputPath?: string, content?: 'embed' | 'attach' | 'omit', omitContent?: boolean } = {}) { +async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise, testInfo: any, options: { outputPath?: string } & Partial> = {}) { const harPath = testInfo.outputPath(options.outputPath || 'test.har'); - const context = await contextFactory({ recordHar: { path: harPath, content: options.content, omitContent: options.omitContent }, ignoreHTTPSErrors: true }); + const context = await contextFactory({ recordHar: { path: harPath, ...options }, ignoreHTTPSErrors: true }); const page = await context.newPage(); return { page, @@ -820,6 +820,47 @@ it('should include API request', async ({ contextFactory, server }, testInfo) => expect(entry.response.headers.find(h => h.name.toLowerCase() === 'content-type')?.value).toContain('application/json'); expect(entry.response.content.size).toBe(15); expect(entry.response.content.text).toBe(responseBody.toString()); + + expect(entry.time).toBeGreaterThan(0); + expect(entry.timings).toEqual(expect.objectContaining({ + blocked: -1, + connect: expect.any(Number), + dns: expect.any(Number), + receive: expect.any(Number), + send: expect.any(Number), + ssl: expect.any(Number), + wait: expect.any(Number), + })); +}); + +it('should respect minimal mode for API Requests', async ({ contextFactory, server }, testInfo) => { + const { page, getLog } = await pageWithHar(contextFactory, testInfo, { mode: 'minimal' }); + const url = server.PREFIX + '/simple.json'; + await page.request.post(url, { + headers: { cookie: 'a=b; c=d' }, + data: { foo: 'bar' } + }); + const { entries } = await getLog(); + expect(entries).toHaveLength(1); + const [entry] = entries; + expect(entry.timings).toEqual({ receive: -1, send: -1, wait: -1 }); +}); + +it('should include redirects from API request', async ({ contextFactory, server }, testInfo) => { + server.setRedirect('/redirect-me', '/simple.json'); + const { page, getLog } = await pageWithHar(contextFactory, testInfo); + await page.request.post(server.PREFIX + '/redirect-me', { + headers: { cookie: 'a=b; c=d' }, + data: { foo: 'bar' } + }); + const log = await getLog(); + expect(log.entries.length).toBe(2); + const [redirect, json] = log.entries; + expect(redirect.request.url).toBe(server.PREFIX + '/redirect-me'); + expect(json.request.url).toBe(server.PREFIX + '/simple.json'); + + expect(redirect.timings).toBeDefined(); + expect(json.timings).toBeDefined(); }); it('should not hang on resources served from cache', async ({ contextFactory, server, browserName }, testInfo) => { From 507e515cb23349f9ba9dfbca1e12b76b39677f7a Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 17 Sep 2024 16:14:24 +0200 Subject: [PATCH 5/6] chore: remove unused @babel/parser (#32654) --- packages/playwright/bundles/babel/package-lock.json | 1 - packages/playwright/bundles/babel/package.json | 1 - packages/playwright/bundles/babel/src/babelBundleImpl.ts | 1 - packages/playwright/src/transform/babelBundle.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/packages/playwright/bundles/babel/package-lock.json b/packages/playwright/bundles/babel/package-lock.json index 0902f0601e..d3531ca0d7 100644 --- a/packages/playwright/bundles/babel/package-lock.json +++ b/packages/playwright/bundles/babel/package-lock.json @@ -11,7 +11,6 @@ "@babel/code-frame": "^7.24.2", "@babel/core": "^7.24.4", "@babel/helper-plugin-utils": "^7.24.0", - "@babel/parser": "^7.24.4", "@babel/plugin-proposal-decorators": "^7.24.1", "@babel/plugin-proposal-explicit-resource-management": "^7.24.1", "@babel/plugin-syntax-async-generators": "^7.8.4", diff --git a/packages/playwright/bundles/babel/package.json b/packages/playwright/bundles/babel/package.json index 27853cf80e..90a0aa85de 100644 --- a/packages/playwright/bundles/babel/package.json +++ b/packages/playwright/bundles/babel/package.json @@ -12,7 +12,6 @@ "@babel/code-frame": "^7.24.2", "@babel/core": "^7.24.4", "@babel/helper-plugin-utils": "^7.24.0", - "@babel/parser": "^7.24.4", "@babel/plugin-proposal-decorators": "^7.24.1", "@babel/plugin-proposal-explicit-resource-management": "^7.24.1", "@babel/plugin-syntax-async-generators": "^7.8.4", diff --git a/packages/playwright/bundles/babel/src/babelBundleImpl.ts b/packages/playwright/bundles/babel/src/babelBundleImpl.ts index f4e44fcea3..82610247f1 100644 --- a/packages/playwright/bundles/babel/src/babelBundleImpl.ts +++ b/packages/playwright/bundles/babel/src/babelBundleImpl.ts @@ -23,7 +23,6 @@ import * as babel from '@babel/core'; export { codeFrameColumns } from '@babel/code-frame'; export { declare } from '@babel/helper-plugin-utils'; export { types } from '@babel/core'; -export { parse } from '@babel/parser'; import traverseFunction from '@babel/traverse'; export const traverse = traverseFunction; diff --git a/packages/playwright/src/transform/babelBundle.ts b/packages/playwright/src/transform/babelBundle.ts index 2806a05aec..faf06b7158 100644 --- a/packages/playwright/src/transform/babelBundle.ts +++ b/packages/playwright/src/transform/babelBundle.ts @@ -18,7 +18,6 @@ import type { BabelFileResult } from '../../bundles/babel/node_modules/@types/ba export const codeFrameColumns: typeof import('../../bundles/babel/node_modules/@types/babel__code-frame').codeFrameColumns = require('./babelBundleImpl').codeFrameColumns; export const declare: typeof import('../../bundles/babel/node_modules/@types/babel__helper-plugin-utils').declare = require('./babelBundleImpl').declare; export const types: typeof import('../../bundles/babel/node_modules/@types/babel__core').types = require('./babelBundleImpl').types; -export const parse: typeof import('../../bundles/babel/node_modules/@babel/parser/typings/babel-parser').parse = require('./babelBundleImpl').parse; export const traverse: typeof import('../../bundles/babel/node_modules/@types/babel__traverse').default = require('./babelBundleImpl').traverse; export type BabelPlugin = [string, any?]; export type BabelTransformFunction = (code: string, filename: string, isTypeScript: boolean, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult; From 8761dafc7365a1340f247472ef9c2c7e36458a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=86=8C=ED=98=84?= <53892427+osohyun0224@users.noreply.github.com> Date: Wed, 18 Sep 2024 00:11:21 +0900 Subject: [PATCH 6/6] feat(test runner): allow to pass arbitrary location to test.step (#32504) Fixes https://github.com/microsoft/playwright/issues/30160 ### Description: This pull request introduces the ability to specify custom locations for test steps in Playwright. By enabling the provision of arbitrary locations to the test.step method, it resolves the limitation where helper methods obfuscate the original call site, providing more accurate and meaningful location data in test reports. ### Motivation: To enhance the utility and clarity of test reports in Playwright. Specifically, it addresses the need to trace test steps back to their precise location in the code, which is especially important when steps are abstracted in helper functions. This feature is crucial for maintaining accurate documentation and facilitating debugging processes. ### Changes: Added functionality to pass a custom location object to test.step. ### Expected Outcome: This PR is expected to significantly improve the precision and usefulness of diagnostic data in test reports by allowing specific locations within helper functions to be accurately documented. It facilitates better tracking of test executions and simplifies the debugging process, making it easier for developers to understand and address issues within complex tests. ### References: Closes https://github.com/microsoft/playwright/issues/30160 - "[Feature]: allow to pass arbitrary location to test.step" **Code Check** I conducted tests on this new feature by integrating it into some existing test codes, and it worked well. I will attach the code used for testing and a screenshot showing the successful outcome.
toggle dropdown
``` import type { Location } from '../../../packages/playwright/types/testReporter' ... test('should respect the back button', async ({ page }) => { await page.locator('.todo-list li .toggle').nth(1).check(); await checkNumberOfCompletedTodosInLocalStorage(page, 1); ... await test.step('Showing active items', async () => { await page.getByRole('link', { name: 'Active' }).click(); }, {location}); ``` image
--- docs/src/test-api/class-test.md | 5 +++ packages/playwright/src/common/testType.ts | 4 +-- packages/playwright/types/test.d.ts | 2 +- tests/playwright-test/test-step.spec.ts | 39 ++++++++++++++++++++++ utils/generate_types/overrides-test.d.ts | 2 +- 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 9b56fbf2e6..b6ae7d1522 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1710,6 +1710,11 @@ Step body. Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. See below for more details. +### option: Test.step.location +* since: v1.48 +- `location` <[Location]> +Specifies a custom location for the step to be shown in test reports. By default, location of the [`method: Test.step`] call is shown. + ## method: Test.use * since: v1.10 diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 5c7850a3df..f0882735dc 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -259,11 +259,11 @@ export class TestTypeImpl { suite._use.push({ fixtures, location }); } - async _step(title: string, body: () => Promise, options: { box?: boolean } = {}): Promise { + async _step(title: string, body: () => Promise, options: {box?: boolean, location?: Location } = {}): Promise { const testInfo = currentTestInfo(); if (!testInfo) throw new Error(`test.step() can only be called from a test`); - const step = testInfo._addStep({ category: 'test.step', title, box: options.box }); + const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); return await zones.run('stepZone', step, async () => { try { const result = await body(); diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index e17a43843c..51a6720a2e 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -4703,7 +4703,7 @@ export interface TestType(title: string, body: () => T | Promise, options?: { box?: boolean }): Promise; + step(title: string, body: () => T | Promise, options?: { box?: boolean, location?: Location }): Promise; /** * `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions). * diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index d14bccd98b..ad3478b112 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -1246,3 +1246,42 @@ fixture | fixture: page fixture | fixture: context `); }); + +test('test location to test.step', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': stepIndentReporter, + 'helper.ts': ` + import { test } from '@playwright/test'; + + export async function dummyStep(test, title, action, location) { + return await test.step(title, action, { location }); + } + + export function getCustomLocation() { + return { file: 'dummy-file.ts', line: 123, column: 45 }; + } + `, + 'playwright.config.ts': ` + module.exports = { + reporter: './reporter', + }; + `, + 'a.test.ts': ` + import { test } from '@playwright/test'; + import { dummyStep, getCustomLocation } from './helper'; + + test('custom location test', async () => { + const location = getCustomLocation(); + await dummyStep(test, 'Perform a dummy step', async () => { + }, location); + }); + ` + }, { reporter: '', workers: 1 }); + + expect(result.exitCode).toBe(0); + expect(stripAnsi(result.output)).toBe(` +hook |Before Hooks +test.step |Perform a dummy step @ dummy-file.ts:123 +hook |After Hooks +`); +}); \ No newline at end of file diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 90ef7fa75a..be1fa7ee37 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -128,7 +128,7 @@ export interface TestType Promise | any): void; afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; - step(title: string, body: () => T | Promise, options?: { box?: boolean }): Promise; + step(title: string, body: () => T | Promise, options?: { box?: boolean, location?: Location }): Promise; expect: Expect<{}>; extend(fixtures: Fixtures): TestType; info(): TestInfo;