From 865f0d822138303eee39070d723878585926e7d7 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 24 Jun 2024 11:28:43 -0700 Subject: [PATCH 1/6] docs(java): correctly parse time (#31420) --- docs/src/api/class-clock.md | 11 ++++++----- docs/src/clock.md | 10 ++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index 1a87cdffa0..6f70e72a02 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -136,7 +136,8 @@ page.clock.pause_at("2020-02-02") ``` ```java -page.clock().pauseAt(Instant.parse("2020-02-02")); +SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd"); +page.clock().pauseAt(format.parse("2020-02-02")); page.clock().pauseAt("2020-02-02"); ``` @@ -182,8 +183,8 @@ page.clock.set_fixed_time("2020-02-02") ``` ```java -page.clock().setFixedTime(Instant.now()); -page.clock().setFixedTime(Instant.parse("2020-02-02")); +page.clock().setFixedTime(new Date()); +page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02")); page.clock().setFixedTime("2020-02-02"); ``` @@ -225,8 +226,8 @@ page.clock.set_system_time("2020-02-02") ``` ```java -page.clock().setSystemTime(Instant.now()); -page.clock().setSystemTime(Instant.parse("2020-02-02")); +page.clock().setSystemTime(new Date()); +page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02")); page.clock().setSystemTime("2020-02-02"); ``` diff --git a/docs/src/clock.md b/docs/src/clock.md index b89dfd93d6..86d0ad9a10 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -118,13 +118,14 @@ expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM" ```java // Initialize clock with some time before the test time and let the page load // naturally. `Date.now` will progress as the timers fire. -page.clock().install(new Clock.InstallOptions().setTime(Instant.parse("2024-02-02T08:00:00"))); +SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss"); +page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-02-02T08:00:00"))); page.navigate("http://localhost:3333"); Locator locator = page.getByTestId("current-time"); // Pretend that the user closed the laptop lid and opened it again at 10am. // Pause the time once reached that point. -page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00")); +page.clock().pauseAt(format.parse("2024-02-02T10:00:00")); // Assert the page state. assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); @@ -315,15 +316,16 @@ expect(locator).to_have_text("2/2/2024, 10:00:02 AM") ``` ```java +SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss"); // Initialize clock with a specific time, let the page load naturally. page.clock().install(new Clock.InstallOptions() - .setTime(Instant.parse("2024-02-02T08:00:00"))); + .setTime(format.parse("2024-02-02T08:00:00"))); page.navigate("http://localhost:3333"); Locator locator = page.getByTestId("current-time"); // Pause the time flow, stop the timers, you now have manual control // over the page time. -page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00")); +page.clock().pauseAt(format.parse("2024-02-02T10:00:00")); assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); // Tick through time manually, firing all timers in the process. From 114b6f0de6d43833f19df70ee842c9cb88595294 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 24 Jun 2024 11:29:40 -0700 Subject: [PATCH 2/6] docs: deprecate `handle` option in `exposeBinding` (#31419) --- docs/src/api/class-browsercontext.md | 78 +---------------------- docs/src/api/class-page.md | 75 +--------------------- packages/playwright-core/types/types.d.ts | 60 ----------------- 3 files changed, 2 insertions(+), 211 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 3ea2180873..686db16f4a 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -748,83 +748,6 @@ await page.SetContentAsync(" -
Click me
-
Or click me
-`); -``` - -```java -context.exposeBinding("clicked", (source, args) -> { - ElementHandle element = (ElementHandle) args[0]; - System.out.println(element.textContent()); - return null; -}, new BrowserContext.ExposeBindingOptions().setHandle(true)); -page.setContent("" + - "\n" + - "
Click me
\n" + - "
Or click me
\n"); -``` - -```python async -async def print(source, element): - print(await element.text_content()) - -await context.expose_binding("clicked", print, handle=true) -await page.set_content(""" - -
Click me
-
Or click me
-""") -``` - -```python sync -def print(source, element): - print(element.text_content()) - -context.expose_binding("clicked", print, handle=true) -page.set_content(""" - -
Click me
-
Or click me
-""") -``` - -```csharp -var result = new TaskCompletionSource(); -var page = await Context.NewPageAsync(); -await Context.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) => -{ - return result.TrySetResult(await t.AsElement().TextContentAsync()); -}); - -await page.SetContentAsync("\n" + - "
Click me
\n" + - "
Or click me
\n"); - -await page.ClickAsync("div"); -// Note: it makes sense to await the result here, because otherwise, the context -// gets closed and the binding function will throw an exception. -Assert.AreEqual("Click me", await result.Task); -``` - ### param: BrowserContext.exposeBinding.name * since: v1.8 - `name` <[string]> @@ -839,6 +762,7 @@ Callback function that will be called in the Playwright's context. ### option: BrowserContext.exposeBinding.handle * since: v1.8 +* deprecated: This option will be removed in the future. - `handle` <[boolean]> Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index d087b78c9f..3d667bb186 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1817,80 +1817,6 @@ class PageExamples } ``` -An example of passing an element handle: - -```js -await page.exposeBinding('clicked', async (source, element) => { - console.log(await element.textContent()); -}, { handle: true }); -await page.setContent(` - -
Click me
-
Or click me
-`); -``` - -```java -page.exposeBinding("clicked", (source, args) -> { - ElementHandle element = (ElementHandle) args[0]; - System.out.println(element.textContent()); - return null; -}, new Page.ExposeBindingOptions().setHandle(true)); -page.setContent("" + - "\n" + - "
Click me
\n" + - "
Or click me
\n"); -``` - -```python async -async def print(source, element): - print(await element.text_content()) - -await page.expose_binding("clicked", print, handle=true) -await page.set_content(""" - -
Click me
-
Or click me
-""") -``` - -```python sync -def print(source, element): - print(element.text_content()) - -page.expose_binding("clicked", print, handle=true) -page.set_content(""" - -
Click me
-
Or click me
-""") -``` - -```csharp -var result = new TaskCompletionSource(); -await page.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) => -{ - return result.TrySetResult(await t.AsElement().TextContentAsync()); -}); - -await page.SetContentAsync("\n" + - "
Click me
\n" + - "
Or click me
\n"); - -await page.ClickAsync("div"); -Console.WriteLine(await result.Task); -``` - ### param: Page.exposeBinding.name * since: v1.8 - `name` <[string]> @@ -1905,6 +1831,7 @@ Callback function that will be called in the Playwright's context. ### option: Page.exposeBinding.handle * since: v1.8 +* deprecated: This option will be removed in the future. - `handle` <[boolean]> Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 67e92a917a..00b20c80c9 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -814,21 +814,6 @@ export interface Page { * })(); * ``` * - * An example of passing an element handle: - * - * ```js - * await page.exposeBinding('clicked', async (source, element) => { - * console.log(await element.textContent()); - * }, { handle: true }); - * await page.setContent(` - * - *
Click me
- *
Or click me
- * `); - * ``` - * * @param name Name of the function on the window object. * @param callback Callback function that will be called in the Playwright's context. * @param options @@ -875,21 +860,6 @@ export interface Page { * })(); * ``` * - * An example of passing an element handle: - * - * ```js - * await page.exposeBinding('clicked', async (source, element) => { - * console.log(await element.textContent()); - * }, { handle: true }); - * await page.setContent(` - * - *
Click me
- *
Or click me
- * `); - * ``` - * * @param name Name of the function on the window object. * @param callback Callback function that will be called in the Playwright's context. * @param options @@ -7637,21 +7607,6 @@ export interface BrowserContext { * })(); * ``` * - * An example of passing an element handle: - * - * ```js - * await context.exposeBinding('clicked', async (source, element) => { - * console.log(await element.textContent()); - * }, { handle: true }); - * await page.setContent(` - * - *
Click me
- *
Or click me
- * `); - * ``` - * * @param name Name of the function on the window object. * @param callback Callback function that will be called in the Playwright's context. * @param options @@ -7693,21 +7648,6 @@ export interface BrowserContext { * })(); * ``` * - * An example of passing an element handle: - * - * ```js - * await context.exposeBinding('clicked', async (source, element) => { - * console.log(await element.textContent()); - * }, { handle: true }); - * await page.setContent(` - * - *
Click me
- *
Or click me
- * `); - * ``` - * * @param name Name of the function on the window object. * @param callback Callback function that will be called in the Playwright's context. * @param options From de723f39e9392d123e53d1f347f69d3d451396a9 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 24 Jun 2024 12:23:46 -0700 Subject: [PATCH 3/6] docs: release notes for 1.45 (#31421) --- docs/src/release-notes-csharp.md | 65 +++++++++++++++++++++++++++++++- docs/src/release-notes-java.md | 63 ++++++++++++++++++++++++++++++- docs/src/release-notes-python.md | 62 +++++++++++++++++++++++++++++- 3 files changed, 187 insertions(+), 3 deletions(-) diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index 9a3301a764..97b0c9cd50 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -4,6 +4,69 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.45 + +### Clock + +Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including: +* testing with predefined time; +* keeping consistent time and timers; +* monitoring inactivity; +* ticking through time manually. + +```csharp +// Initialize clock with some time before the test time and let the page load naturally. +// `Date.now` will progress as the timers fire. +await Page.Clock.InstallAsync(new +{ + Time = new DateTime(2024, 2, 2, 8, 0, 0) +}); +await Page.GotoAsync("http://localhost:3333"); + +// Pretend that the user closed the laptop lid and opened it again at 10am. +// Pause the time once reached that point. +await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0)); + +// Assert the page state. +await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM"); + +// Close the laptop lid again and open it at 10:30am. +await Page.Clock.FastForwardAsync("30:00"); +await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:30:00 AM"); +``` + +See [the clock guide](./clock.md) for more details. + +### Miscellaneous + +- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `` elements. + ```csharp + await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir"); + ``` + +- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux. + ```csharp + // Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation. + await page.Keyboard.PressAsync("ControlOrMeta+S"); + ``` + +- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`. + +- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04. + +- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit. + +### Browser Versions + +* Chromium 127.0.6533.5 +* Mozilla Firefox 127.0 +* WebKit 17.4 + +This version was also tested against the following stable channels: + +* Google Chrome 126 +* Microsoft Edge 126 + ## Version 1.44 ### New APIs @@ -129,7 +192,7 @@ This version was also tested against the following stable channels: ### New Locator Handler New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears. - + ```csharp // Setup the handler. await Page.AddLocatorHandlerAsync( diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index 4a46556f68..6dce675b8f 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -4,6 +4,67 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.45 + +### Clock + +Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including: +* testing with predefined time; +* keeping consistent time and timers; +* monitoring inactivity; +* ticking through time manually. + +```java +// Initialize clock with some time before the test time and let the page load +// naturally. `Date.now` will progress as the timers fire. +page.clock().install(new Clock.InstallOptions().setTime("2024-02-02T08:00:00")); +page.navigate("http://localhost:3333"); +Locator locator = page.getByTestId("current-time"); + +// Pretend that the user closed the laptop lid and opened it again at 10am. +// Pause the time once reached that point. +page.clock().pauseAt("2024-02-02T10:00:00"); + +// Assert the page state. +assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); + +// Close the laptop lid again and open it at 10:30am. +page.clock().fastForward("30:00"); +assertThat(locator).hasText("2/2/2024, 10:30:00 AM"); +``` + +See [the clock guide](./clock.md) for more details. + +### Miscellaneous + +- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `` elements. + ```java + page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir")); + ``` + +- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux. + ```java + // Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation. + page.keyboard.press("ControlOrMeta+S"); + ``` + +- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`. + +- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04. + +- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit. + +### Browser Versions + +* Chromium 127.0.6533.5 +* Mozilla Firefox 127.0 +* WebKit 17.4 + +This version was also tested against the following stable channels: + +* Google Chrome 126 +* Microsoft Edge 126 + ## Version 1.44 ### New APIs @@ -201,7 +262,7 @@ Learn more about the fixtures in our [JUnit guide](./junit.md). ### New Locator Handler New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears. - + ```java // Setup the handler. page.addLocatorHandler( diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index 590a4edb88..e7a4341a7e 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -4,6 +4,66 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.45 + +### Clock + +Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including: +* testing with predefined time; +* keeping consistent time and timers; +* monitoring inactivity; +* ticking through time manually. + +```python +# Initialize clock with some time before the test time and let the page load +# naturally. `Date.now` will progress as the timers fire. +page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0)) +page.goto("http://localhost:3333") + +# Pretend that the user closed the laptop lid and opened it again at 10am. +# Pause the time once reached that point. +page.clock.pause_at(datetime.datetime(2024, 2, 2, 10, 0, 0)) + +# Assert the page state. +expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM") + +# Close the laptop lid again and open it at 10:30am. +page.clock.fast_forward("30:00") +expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM") +``` + +See [the clock guide](./clock.md) for more details. + +### Miscellaneous + +- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `` elements. + ```python + page.get_by_label("Upload directory").set_input_files('mydir') + ``` + +- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux. + ```python + # Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation. + page.keyboard.press("ControlOrMeta+S") + ``` + +- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`. + +- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04. + +- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit. + +### Browser Versions + +* Chromium 127.0.6533.5 +* Mozilla Firefox 127.0 +* WebKit 17.4 + +This version was also tested against the following stable channels: + +* Google Chrome 126 +* Microsoft Edge 126 + ## Version 1.44 ### New APIs @@ -109,7 +169,7 @@ This version was also tested against the following stable channels: ### New Locator Handler New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears. - + ```python # Setup the handler. page.add_locator_handler( From 2b12f53332abc0cac39026b1b2dea347bd1b497b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 24 Jun 2024 12:25:12 -0700 Subject: [PATCH 4/6] chore(route): wait for raw headers from browser in case of redirects (#31410) Redirects are always autoresumed, so the will always receive extra info with raw headers. We only want to make raw headers available immediately when there is a route. Reference https://github.com/microsoft/playwright/issues/31351 --- .../playwright-core/src/server/chromium/crNetworkManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index 42dd6a640e..850f466afa 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -359,11 +359,11 @@ export class CRNetworkManager { }); this._requestIdToRequest.set(requestWillBeSentEvent.requestId, request); - if (requestPausedEvent) { - // We will not receive extra info when intercepting the request. + if (route) { + // We may not receive extra info when intercepting the request. // Use the headers from the Fetch.requestPausedPayload and release the allHeaders() // right away, so that client can call it from the route handler. - request.request.setRawRequestHeaders(headersOverride ?? headersObjectToArray(requestPausedEvent.request.headers, '\n')); + request.request.setRawRequestHeaders(headersObjectToArray(requestPausedEvent!.request.headers, '\n')); } (this._page?._frameManager || this._serviceWorker)!.requestStarted(request.request, route || undefined); } From 47fb9a080d9dd4c1dff062c56060b86b621485ac Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 24 Jun 2024 23:34:17 +0200 Subject: [PATCH 5/6] fix(test-runner): don't add slow annotation twice (#31414) --- packages/playwright/src/worker/workerMain.ts | 2 +- tests/playwright-test/test-modifiers.spec.ts | 25 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index 3237104520..e97bf2945c 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -261,7 +261,7 @@ export class WorkerMain extends ProcessRunner { testInfo.expectedStatus = 'failed'; break; case 'slow': - testInfo.slow(); + testInfo._timeoutManager.slow(); break; } }; diff --git a/tests/playwright-test/test-modifiers.spec.ts b/tests/playwright-test/test-modifiers.spec.ts index 2b206b448a..2d11ad0c9a 100644 --- a/tests/playwright-test/test-modifiers.spec.ts +++ b/tests/playwright-test/test-modifiers.spec.ts @@ -690,3 +690,28 @@ test('static modifiers should be added in serial mode', async ({ runInlineTest } expect(result.report.suites[0].specs[2].tests[0].annotations).toEqual([{ type: 'skip' }]); expect(result.report.suites[0].specs[3].tests[0].annotations).toEqual([]); }); + +test('should contain only one slow modifier', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'slow.test.ts': ` + import { test } from '@playwright/test'; + test.slow(); + test('pass', { annotation: { type: 'issue', description: 'my-value' } }, () => {}); + `, + 'skip.test.ts': ` + import { test } from '@playwright/test'; + test.skip(); + test('pass', { annotation: { type: 'issue', description: 'my-value' } }, () => {}); + `, + 'fixme.test.ts': ` + import { test } from '@playwright/test'; + test.fixme(); + test('pass', { annotation: { type: 'issue', description: 'my-value' } }, () => {}); +`, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'fixme' }, { type: 'issue', description: 'my-value' }]); + expect(result.report.suites[1].specs[0].tests[0].annotations).toEqual([{ type: 'skip' }, { type: 'issue', description: 'my-value' }]); + expect(result.report.suites[2].specs[0].tests[0].annotations).toEqual([{ type: 'slow' }, { type: 'issue', description: 'my-value' }]); +}); From 122818c62c129455862246a2f6cac57e8fb39321 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 24 Jun 2024 21:43:43 -0700 Subject: [PATCH 6/6] feat: allow boxing and titling fixtures, simulate context fixture deps (#31423) Fixes https://github.com/microsoft/playwright/issues/31411 --- docs/src/test-fixtures-js.md | 464 ++---------------- packages/playwright/src/common/fixtures.ts | 68 ++- packages/playwright/src/index.ts | 18 +- .../playwright/src/worker/fixtureRunner.ts | 6 +- packages/playwright/types/test.d.ts | 8 +- tests/playwright-test/fixture-errors.spec.ts | 35 ++ tests/playwright-test/timeout.spec.ts | 4 +- utils/generate_types/overrides-test.d.ts | 8 +- 8 files changed, 146 insertions(+), 465 deletions(-) diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md index 0971922d35..57334aa2ef 100644 --- a/docs/src/test-fixtures-js.md +++ b/docs/src/test-fixtures-js.md @@ -43,48 +43,7 @@ Here is how typical test environment setup differs between traditional test styl Click to expand the code for the TodoPage
-```js tab=js-js title="todo-page.js" -export class TodoPage { - /** - * @param {import('@playwright/test').Page} page - */ - constructor(page) { - this.page = page; - this.inputBox = this.page.locator('input.new-todo'); - this.todoItems = this.page.getByTestId('todo-item'); - } - - async goto() { - await this.page.goto('https://demo.playwright.dev/todomvc/'); - } - - /** - * @param {string} text - */ - async addToDo(text) { - await this.inputBox.fill(text); - await this.inputBox.press('Enter'); - } - - /** - * @param {string} text - */ - async remove(text) { - const todo = this.todoItems.filter({ hasText: text }); - await todo.hover(); - await todo.getByLabel('Delete').click(); - } - - async removeAll() { - while ((await this.todoItems.count()) > 0) { - await this.todoItems.first().hover(); - await this.todoItems.getByLabel('Delete').first().click(); - } - } -} -``` - -```js tab=js-ts title="todo-page.ts" +```js title="todo-page.ts" import type { Page, Locator } from '@playwright/test'; export class TodoPage { @@ -167,48 +126,7 @@ Fixtures have a number of advantages over before/after hooks: Click to expand the code for the TodoPage
-```js tab=js-js title="todo-page.js" -export class TodoPage { - /** - * @param {import('@playwright/test').Page} page - */ - constructor(page) { - this.page = page; - this.inputBox = this.page.locator('input.new-todo'); - this.todoItems = this.page.getByTestId('todo-item'); - } - - async goto() { - await this.page.goto('https://demo.playwright.dev/todomvc/'); - } - - /** - * @param {string} text - */ - async addToDo(text) { - await this.inputBox.fill(text); - await this.inputBox.press('Enter'); - } - - /** - * @param {string} text - */ - async remove(text) { - const todo = this.todoItems.filter({ hasText: text }); - await todo.hover(); - await todo.getByLabel('Delete').click(); - } - - async removeAll() { - while ((await this.todoItems.count()) > 0) { - await this.todoItems.first().hover(); - await this.todoItems.getByLabel('Delete').first().click(); - } - } -} -``` - -```js tab=js-ts title="todo-page.ts" +```js title="todo-page.ts" import type { Page, Locator } from '@playwright/test'; export class TodoPage { @@ -246,34 +164,7 @@ export class TodoPage {
-```js tab=js-js title="todo.spec.js" -const base = require('@playwright/test'); -const { TodoPage } = require('./todo-page'); - -// Extend basic test by providing a "todoPage" fixture. -const test = base.test.extend({ - todoPage: async ({ page }, use) => { - const todoPage = new TodoPage(page); - await todoPage.goto(); - await todoPage.addToDo('item1'); - await todoPage.addToDo('item2'); - await use(todoPage); - await todoPage.removeAll(); - }, -}); - -test('should add an item', async ({ todoPage }) => { - await todoPage.addToDo('my item'); - // ... -}); - -test('should remove an item', async ({ todoPage }) => { - await todoPage.remove('item1'); - // ... -}); -``` - -```js tab=js-ts title="example.spec.ts" +```js title="example.spec.ts" import { test as base } from '@playwright/test'; import { TodoPage } from './todo-page'; @@ -309,48 +200,8 @@ Below we create two fixtures `todoPage` and `settingsPage` that follow the [Page
Click to expand the code for the TodoPage and SettingsPage
-```js tab=js-js title="todo-page.js" -export class TodoPage { - /** - * @param {import('@playwright/test').Page} page - */ - constructor(page) { - this.page = page; - this.inputBox = this.page.locator('input.new-todo'); - this.todoItems = this.page.getByTestId('todo-item'); - } - async goto() { - await this.page.goto('https://demo.playwright.dev/todomvc/'); - } - - /** - * @param {string} text - */ - async addToDo(text) { - await this.inputBox.fill(text); - await this.inputBox.press('Enter'); - } - - /** - * @param {string} text - */ - async remove(text) { - const todo = this.todoItems.filter({ hasText: text }); - await todo.hover(); - await todo.getByLabel('Delete').click(); - } - - async removeAll() { - while ((await this.todoItems.count()) > 0) { - await this.todoItems.first().hover(); - await this.todoItems.getByLabel('Delete').first().click(); - } - } -} -``` - -```js tab=js-ts title="todo-page.ts" +```js title="todo-page.ts" import type { Page, Locator } from '@playwright/test'; export class TodoPage { @@ -388,22 +239,7 @@ export class TodoPage { SettingsPage is similar: -```js tab=js-js title="settings-page.js" -export class SettingsPage { - /** - * @param {import('@playwright/test').Page} page - */ - constructor(page) { - this.page = page; - } - - async switchToDarkMode() { - // ... - } -} -``` - -```js tab=js-ts title="settings-page.ts" +```js title="settings-page.ts" import type { Page } from '@playwright/test'; export class SettingsPage { @@ -419,36 +255,7 @@ export class SettingsPage {
-```js tab=js-js title="my-test.js" -const base = require('@playwright/test'); -const { TodoPage } = require('./todo-page'); -const { SettingsPage } = require('./settings-page'); - -// Extend base test by providing "todoPage" and "settingsPage". -// This new "test" can be used in multiple test files, and each of them will get the fixtures. -exports.test = base.test.extend({ - todoPage: async ({ page }, use) => { - // Set up the fixture. - const todoPage = new TodoPage(page); - await todoPage.goto(); - await todoPage.addToDo('item1'); - await todoPage.addToDo('item2'); - - // Use the fixture value in the test. - await use(todoPage); - - // Clean up the fixture. - await todoPage.removeAll(); - }, - - settingsPage: async ({ page }, use) => { - await use(new SettingsPage(page)); - }, -}); -exports.expect = base.expect; -``` - -```js tab=js-ts title="my-test.ts" +```js title="my-test.ts" import { test as base } from '@playwright/test'; import { TodoPage } from './todo-page'; import { SettingsPage } from './settings-page'; @@ -493,20 +300,7 @@ Just mention fixture in your test function argument, and test runner will take c Below we use the `todoPage` and `settingsPage` fixtures defined above. -```js tab=js-js -const { test, expect } = require('./my-test'); - -test.beforeEach(async ({ settingsPage }) => { - await settingsPage.switchToDarkMode(); -}); - -test('basic test', async ({ todoPage, page }) => { - await todoPage.addToDo('something nice'); - await expect(page.getByTestId('todo-title')).toContainText(['something nice']); -}); -``` - -```js tab=js-ts +```js import { test, expect } from './my-test'; test.beforeEach(async ({ settingsPage }) => { @@ -560,47 +354,7 @@ Playwright Test uses [worker processes](./test-parallel.md) to run test files. S Below we'll create an `account` fixture that will be shared by all tests in the same worker, and override the `page` fixture to login into this account for each test. To generate unique accounts, we'll use the [`property: WorkerInfo.workerIndex`] that is available to any test or fixture. Note the tuple-like syntax for the worker fixture - we have to pass `{scope: 'worker'}` so that test runner sets up this fixture once per worker. -```js tab=js-js title="my-test.js" -const base = require('@playwright/test'); - -exports.test = base.test.extend({ - account: [async ({ browser }, use, workerInfo) => { - // Unique username. - const username = 'user' + workerInfo.workerIndex; - const password = 'verysecure'; - - // Create the account with Playwright. - const page = await browser.newPage(); - await page.goto('/signup'); - await page.getByLabel('User Name').fill(username); - await page.getByLabel('Password').fill(password); - await page.getByText('Sign up').click(); - // Make sure everything is ok. - await expect(page.locator('#result')).toHaveText('Success'); - // Do not forget to cleanup. - await page.close(); - - // Use the account value. - await use({ username, password }); - }, { scope: 'worker' }], - - page: async ({ page, account }, use) => { - // Sign in with our account. - const { username, password } = account; - await page.goto('/signin'); - await page.getByLabel('User Name').fill(username); - await page.getByLabel('Password').fill(password); - await page.getByText('Sign in').click(); - await expect(page.getByTestId('userinfo')).toHaveText(username); - - // Use signed-in page in the test. - await use(page); - }, -}); -exports.expect = base.expect; -``` - -```js tab=js-ts title="my-test.ts" +```js title="my-test.ts" import { test as base } from '@playwright/test'; type Account = { @@ -652,32 +406,7 @@ Automatic fixtures are set up for each test/worker, even when the test does not Here is an example fixture that automatically attaches debug logs when the test fails, so we can later review the logs in the reporter. Note how it uses [TestInfo] object that is available in each test/fixture to retrieve metadata about the test being run. -```js tab=js-js title="my-test.js" -const debug = require('debug'); -const fs = require('fs'); -const base = require('@playwright/test'); - -exports.test = base.test.extend({ - saveLogs: [async ({}, use, testInfo) => { - // Collecting logs during the test. - const logs = []; - debug.log = (...args) => logs.push(args.map(String).join('')); - debug.enable('myserver'); - - await use(); - - // After the test we can check whether the test passed or failed. - if (testInfo.status !== testInfo.expectedStatus) { - // outputPath() API guarantees a unique file name. - const logFile = testInfo.outputPath('logs.txt'); - await fs.promises.writeFile(logFile, logs.join('\n'), 'utf8'); - testInfo.attachments.push({ name: 'logs', contentType: 'text/plain', path: logFile }); - } - }, { auto: true }], -}); -``` - -```js tab=js-ts title="my-test.ts" +```js title="my-test.ts" import * as debug from 'debug'; import * as fs from 'fs'; import { test as base } from '@playwright/test'; @@ -707,22 +436,7 @@ export { expect } from '@playwright/test'; By default, fixture shares timeout with the test. However, for slow fixtures, especially [worker-scoped](#worker-scoped-fixtures) ones, it is convenient to have a separate timeout. This way you can keep the overall test timeout small, and give the slow fixture more time. -```js tab=js-js -const { test: base, expect } = require('@playwright/test'); - -const test = base.extend({ - slowFixture: [async ({}, use) => { - // ... perform a slow operation ... - await use('hello'); - }, { timeout: 60000 }] -}); - -test('example test', async ({ slowFixture }) => { - // ... -}); -``` - -```js tab=js-ts +```js import { test as base, expect } from '@playwright/test'; const test = base.extend<{ slowFixture: string }>({ @@ -752,48 +466,7 @@ Below we'll create a `defaultItem` option in addition to the `todoPage` fixture Click to expand the code for the TodoPage
-```js tab=js-js title="todo-page.js" -export class TodoPage { - /** - * @param {import('@playwright/test').Page} page - */ - constructor(page) { - this.page = page; - this.inputBox = this.page.locator('input.new-todo'); - this.todoItems = this.page.getByTestId('todo-item'); - } - - async goto() { - await this.page.goto('https://demo.playwright.dev/todomvc/'); - } - - /** - * @param {string} text - */ - async addToDo(text) { - await this.inputBox.fill(text); - await this.inputBox.press('Enter'); - } - - /** - * @param {string} text - */ - async remove(text) { - const todo = this.todoItems.filter({ hasText: text }); - await todo.hover(); - await todo.getByLabel('Delete').click(); - } - - async removeAll() { - while ((await this.todoItems.count()) > 0) { - await this.todoItems.first().hover(); - await this.todoItems.getByLabel('Delete').first().click(); - } - } -} -``` - -```js tab=js-ts title="todo-page.ts" +```js title="todo-page.ts" import type { Page, Locator } from '@playwright/test'; export class TodoPage { @@ -832,28 +505,7 @@ export class TodoPage {
-```js tab=js-js title="my-test.js" -const base = require('@playwright/test'); -const { TodoPage } = require('./todo-page'); - -exports.test = base.test.extend({ - // Define an option and provide a default value. - // We can later override it in the config. - defaultItem: ['Something nice', { option: true }], - - // Our "todoPage" fixture depends on the option. - todoPage: async ({ page, defaultItem }, use) => { - const todoPage = new TodoPage(page); - await todoPage.goto(); - await todoPage.addToDo(defaultItem); - await use(todoPage); - await todoPage.removeAll(); - }, -}); -exports.expect = base.expect; -``` - -```js tab=js-ts title="my-test.ts" +```js title="my-test.ts" import { test as base } from '@playwright/test'; import { TodoPage } from './todo-page'; @@ -885,25 +537,7 @@ export { expect } from '@playwright/test'; We can now use `todoPage` fixture as usual, and set the `defaultItem` option in the config file. -```js tab=js-js title="playwright.config.ts" -// @ts-check - -const { defineConfig } = require('@playwright/test'); -module.exports = defineConfig({ - projects: [ - { - name: 'shopping', - use: { defaultItem: 'Buy milk' }, - }, - { - name: 'wellbeing', - use: { defaultItem: 'Exercise!' }, - }, - ] -}); -``` - -```js tab=js-ts title="playwright.config.ts" +```js title="playwright.config.ts" import { defineConfig } from '@playwright/test'; import type { MyOptions } from './my-test'; @@ -932,50 +566,7 @@ Fixtures follow these rules to determine the execution order: Consider the following example: -```js tab=js-js -const { test: base } = require('@playwright/test'); - -const test = base.extend({ - workerFixture: [async ({ browser }) => { - // workerFixture setup... - await use('workerFixture'); - // workerFixture teardown... - }, { scope: 'worker' }], - - autoWorkerFixture: [async ({ browser }) => { - // autoWorkerFixture setup... - await use('autoWorkerFixture'); - // autoWorkerFixture teardown... - }, { scope: 'worker', auto: true }], - - testFixture: [async ({ page, workerFixture }) => { - // testFixture setup... - await use('testFixture'); - // testFixture teardown... - }, { scope: 'test' }], - - autoTestFixture: [async () => { - // autoTestFixture setup... - await use('autoTestFixture'); - // autoTestFixture teardown... - }, { scope: 'test', auto: true }], - - unusedFixture: [async ({ page }) => { - // unusedFixture setup... - await use('unusedFixture'); - // unusedFixture teardown... - }, { scope: 'test' }], -}); - -test.beforeAll(async () => { /* ... */ }); -test.beforeEach(async ({ page }) => { /* ... */ }); -test('first test', async ({ page }) => { /* ... */ }); -test('second test', async ({ testFixture }) => { /* ... */ }); -test.afterEach(async () => { /* ... */ }); -test.afterAll(async () => { /* ... */ }); -``` - -```js tab=js-ts +```js import { test as base } from '@playwright/test'; const test = base.extend<{ @@ -1081,3 +672,30 @@ test('passes', async ({ database, page, a11y }) => { // use database and a11y fixtures. }); ``` + +## Box fixtures + +You can minimize the fixture exposure to the reporters UI and error messages via boxing it: + +```js +import { test as base } from '@playwright/test'; + +export const test = base.extend({ + _helperFixture: [async ({}, use, testInfo) => { + }, { box: true }], +}); +``` + +## Custom fixture title + +You can assign a custom title to a fixture to be used in error messages and in the +reporters UI: + +```js +import { test as base } from '@playwright/test'; + +export const test = base.extend({ + _innerFixture: [async ({}, use, testInfo) => { + }, { title: 'my fixture' }], +}); +``` diff --git a/packages/playwright/src/common/fixtures.ts b/packages/playwright/src/common/fixtures.ts index aa3886718c..6b50947ab5 100644 --- a/packages/playwright/src/common/fixtures.ts +++ b/packages/playwright/src/common/fixtures.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { formatLocation } from '../util'; +import { filterStackFile, formatLocation } from '../util'; import * as crypto from 'crypto'; import type { Fixtures } from '../../types/test'; import type { Location } from '../../types/testReporter'; @@ -23,7 +23,7 @@ import type { FixturesWithLocation } from './config'; export type FixtureScope = 'test' | 'worker'; type FixtureAuto = boolean | 'all-hooks-included'; const kScopeOrder: FixtureScope[] = ['test', 'worker']; -type FixtureOptions = { auto?: FixtureAuto, scope?: FixtureScope, option?: boolean, timeout?: number | undefined }; +type FixtureOptions = { auto?: FixtureAuto, scope?: FixtureScope, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }; type FixtureTuple = [ value: any, options: FixtureOptions ]; export type FixtureRegistration = { // Fixture registration location. @@ -49,8 +49,8 @@ export type FixtureRegistration = { super?: FixtureRegistration; // Whether this fixture is an option override value set from the config. optionOverride?: boolean; - // Do not generate the step for this fixture. - hideStep?: boolean; + // Do not generate the step for this fixture, consider it internal. + box?: boolean; }; export type LoadError = { message: string; @@ -63,7 +63,7 @@ type OptionOverrides = { }; function isFixtureTuple(value: any): value is FixtureTuple { - return Array.isArray(value) && typeof value[1] === 'object' && ('scope' in value[1] || 'auto' in value[1] || 'option' in value[1] || 'timeout' in value[1]); + return Array.isArray(value) && typeof value[1] === 'object'; } function isFixtureOption(value: any): value is FixtureTuple { @@ -103,15 +103,15 @@ export class FixturePool { for (const entry of Object.entries(fixtures)) { const name = entry[0]; let value = entry[1]; - let options: { auto: FixtureAuto, scope: FixtureScope, option: boolean, timeout: number | undefined, customTitle: string | undefined, hideStep: boolean | undefined } | undefined; + let options: { auto: FixtureAuto, scope: FixtureScope, option: boolean, timeout: number | undefined, customTitle?: string, box?: boolean } | undefined; if (isFixtureTuple(value)) { options = { auto: value[1].auto ?? false, scope: value[1].scope || 'test', option: !!value[1].option, timeout: value[1].timeout, - customTitle: (value[1] as any)._title, - hideStep: (value[1] as any)._hideStep, + customTitle: value[1].title, + box: value[1].box, }; value = value[0]; } @@ -128,9 +128,9 @@ export class FixturePool { continue; } } else if (previous) { - options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle, hideStep: undefined }; + options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle, box: previous.box }; } else if (!options) { - options = { auto: false, scope: 'test', option: false, timeout: undefined, customTitle: undefined, hideStep: undefined }; + options = { auto: false, scope: 'test', option: false, timeout: undefined }; } if (!kScopeOrder.includes(options.scope)) { @@ -152,7 +152,7 @@ export class FixturePool { } const deps = fixtureParameterNames(fn, location, e => this._onLoadError(e)); - const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, option: options.option, timeout: options.timeout, customTitle: options.customTitle, hideStep: options.hideStep, deps, super: previous, optionOverride: isOptionsOverride }; + const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, option: options.option, timeout: options.timeout, customTitle: options.customTitle, box: options.box, deps, super: previous, optionOverride: isOptionsOverride }; registrationId(registration); this._registrations.set(name, registration); } @@ -161,29 +161,36 @@ export class FixturePool { private validate() { const markers = new Map(); const stack: FixtureRegistration[] = []; - const visit = (registration: FixtureRegistration) => { + let hasDependencyErrors = false; + const addDependencyError = (message: string, location: Location) => { + hasDependencyErrors = true; + this._addLoadError(message, location); + }; + const visit = (registration: FixtureRegistration, boxedOnly: boolean) => { markers.set(registration, 'visiting'); stack.push(registration); for (const name of registration.deps) { const dep = this.resolve(name, registration); if (!dep) { if (name === registration.name) - this._addLoadError(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration.location); + addDependencyError(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration.location); else - this._addLoadError(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration.location); + addDependencyError(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration.location); continue; } if (kScopeOrder.indexOf(registration.scope) > kScopeOrder.indexOf(dep.scope)) { - this._addLoadError(`${registration.scope} fixture "${registration.name}" cannot depend on a ${dep.scope} fixture "${name}" defined in ${formatLocation(dep.location)}.`, registration.location); + addDependencyError(`${registration.scope} fixture "${registration.name}" cannot depend on a ${dep.scope} fixture "${name}" defined in ${formatPotentiallyInternalLocation(dep.location)}.`, registration.location); continue; } if (!markers.has(dep)) { - visit(dep); + visit(dep, boxedOnly); } else if (markers.get(dep) === 'visiting') { const index = stack.indexOf(dep); - const regs = stack.slice(index, stack.length); + const allRegs = stack.slice(index, stack.length); + const filteredRegs = allRegs.filter(r => !r.box); + const regs = boxedOnly ? filteredRegs : allRegs; const names = regs.map(r => `"${r.name}"`); - this._addLoadError(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle: ${regs.map(r => formatLocation(r.location)).join(' -> ')}`, dep.location); + addDependencyError(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle: ${regs.map(r => formatPotentiallyInternalLocation(r.location)).join(' -> ')} -> ${formatPotentiallyInternalLocation(dep.location)}`, dep.location); continue; } } @@ -191,11 +198,27 @@ export class FixturePool { stack.pop(); }; - const hash = crypto.createHash('sha1'); const names = Array.from(this._registrations.keys()).sort(); + + // First iterate over non-boxed fixtures to provide clear error messages. + for (const name of names) { + const registration = this._registrations.get(name)!; + if (!registration.box) + visit(registration, true); + } + + // If no errors found, iterate over boxed fixtures + if (!hasDependencyErrors) { + for (const name of names) { + const registration = this._registrations.get(name)!; + if (registration.box) + visit(registration, false); + } + } + + const hash = crypto.createHash('sha1'); for (const name of names) { const registration = this._registrations.get(name)!; - visit(registration); if (registration.scope === 'worker') hash.update(registration.id + ';'); } @@ -227,6 +250,11 @@ export class FixturePool { const signatureSymbol = Symbol('signature'); +export function formatPotentiallyInternalLocation(location: Location): string { + const isUserFixture = location && filterStackFile(location.file); + return isUserFixture ? formatLocation(location) : ''; +} + export function fixtureParameterNames(fn: Function | any, location: Location, onError: LoadErrorSink): string[] { if (typeof fn !== 'function') return []; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 3fbaf33585..2e1236d8ea 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -59,13 +59,13 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { const playwrightFixtures: Fixtures = ({ defaultBrowserType: ['chromium', { scope: 'worker', option: true }], browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true }], - _playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker' }], + _playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker', box: true }], playwright: [async ({ _playwrightImpl, screenshot }, use) => { await connector.setPlaywright(_playwrightImpl, screenshot); await use(_playwrightImpl); await connector.setPlaywright(undefined, screenshot); - }, { scope: 'worker', _hideStep: true } as any], + }, { scope: 'worker', box: true }], headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }], channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }], @@ -93,7 +93,7 @@ const playwrightFixtures: Fixtures = ({ await use(options); for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) (browserType as any)._defaultLaunchOptions = undefined; - }, { scope: 'worker', auto: true }], + }, { scope: 'worker', auto: true, box: true }], browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => { if (!['chromium', 'firefox', 'webkit'].includes(browserName)) @@ -152,7 +152,7 @@ const playwrightFixtures: Fixtures = ({ serviceWorkers: [({ contextOptions }, use) => use(contextOptions.serviceWorkers ?? 'allow'), { option: true }], contextOptions: [{}, { option: true }], - _combinedContextOptions: async ({ + _combinedContextOptions: [async ({ acceptDownloads, bypassCSP, colorScheme, @@ -223,7 +223,7 @@ const playwrightFixtures: Fixtures = ({ ...contextOptions, ...options, }); - }, + }, { box: true }], _setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => { if (testIdAttribute) @@ -246,9 +246,9 @@ const playwrightFixtures: Fixtures = ({ (browserType as any)._defaultContextTimeout = undefined; (browserType as any)._defaultContextNavigationTimeout = undefined; } - }, { auto: 'all-hooks-included', _title: 'context configuration' } as any], + }, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any], - _contextFactory: [async ({ browser, video, _reuseContext }, use, testInfo) => { + _contextFactory: [async ({ browser, video, _reuseContext, _combinedContextOptions /** mitigate dep-via-auto lack of traceability */ }, use, testInfo) => { const testInfoImpl = testInfo as TestInfoImpl; const videoMode = normalizeVideoMode(video); const captureVideo = shouldCaptureVideo(videoMode, testInfo) && !_reuseContext; @@ -301,7 +301,7 @@ const playwrightFixtures: Fixtures = ({ } })); - }, { scope: 'test', _title: 'context' } as any], + }, { scope: 'test', title: 'context', box: true }], _optionContextReuseMode: ['none', { scope: 'worker', option: true }], _optionConnectOptions: [undefined, { scope: 'worker', option: true }], @@ -312,7 +312,7 @@ const playwrightFixtures: Fixtures = ({ mode = 'when-possible'; const reuse = mode === 'when-possible' && normalizeVideoMode(video) === 'off'; await use(reuse); - }, { scope: 'worker', _title: 'context' } as any], + }, { scope: 'worker', title: 'context', box: true }], context: async ({ playwright, browser, _reuseContext, _contextFactory }, use, testInfo) => { attachConnectedHeaderIfNeeded(testInfo, browser); diff --git a/packages/playwright/src/worker/fixtureRunner.ts b/packages/playwright/src/worker/fixtureRunner.ts index 8832acf8e2..0d94f40631 100644 --- a/packages/playwright/src/worker/fixtureRunner.ts +++ b/packages/playwright/src/worker/fixtureRunner.ts @@ -40,10 +40,10 @@ class Fixture { this.runner = runner; this.registration = registration; this.value = null; - const shouldGenerateStep = !this.registration.hideStep && !this.registration.name.startsWith('_') && !this.registration.option; - const isInternalFixture = this.registration.location && filterStackFile(this.registration.location.file); + const shouldGenerateStep = !this.registration.box && !this.registration.option; + const isUserFixture = this.registration.location && filterStackFile(this.registration.location.file); const title = this.registration.customTitle || this.registration.name; - const location = isInternalFixture ? this.registration.location : undefined; + const location = isUserFixture ? this.registration.location : undefined; this._stepInfo = shouldGenerateStep ? { category: 'fixture', location } : undefined; this._setupDescription = { title, diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 2fd171e3bd..5a18eded3f 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -4811,13 +4811,13 @@ export type WorkerFixture = (args: Args, use: (r: R) = type TestFixtureValue = Exclude | TestFixture; type WorkerFixtureValue = Exclude | WorkerFixture; export type Fixtures = { - [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined }]; + [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }]; } & { - [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined }]; + [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }]; } & { - [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined }]; + [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; } & { - [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }]; + [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; }; type BrowserName = 'chromium' | 'firefox' | 'webkit'; diff --git a/tests/playwright-test/fixture-errors.spec.ts b/tests/playwright-test/fixture-errors.spec.ts index c17db65bce..51c928f156 100644 --- a/tests/playwright-test/fixture-errors.spec.ts +++ b/tests/playwright-test/fixture-errors.spec.ts @@ -256,6 +256,41 @@ test('should detect fixture dependency cycle', async ({ runInlineTest }) => { expect(result.exitCode).toBe(1); }); +test('should hide boxed fixtures in dependency cycle', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'x.spec.ts': ` + import { test as base } from '@playwright/test'; + const test = base.extend({ + storageState: async ({ context, storageState }, use) => { + await use(storageState); + } + }); + test('failed', async ({ page }) => {}); + `, + }); + expect(result.output).toContain('Fixtures "context" -> "storageState" -> "context" form a dependency cycle: -> x.spec.ts:3:25 -> '); + expect(result.exitCode).toBe(1); +}); + +test('should show boxed fixtures in dependency cycle if there are no public fixtures', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'x.spec.ts': ` + import { test as base } from '@playwright/test'; + const test = base.extend({ + f1: [async ({ f2 }, use) => { + await use(f2); + }, { box: true }], + f2: [async ({ f1 }, use) => { + await use(f1); + }, { box: true }], + }); + test('failed', async ({ f1, f2 }) => {}); + `, + }); + expect(result.output).toContain('Fixtures "f1" -> "f2" -> "f1" form a dependency cycle: x.spec.ts:3:25 -> x.spec.ts:3:25 -> x.spec.ts:3:25'); + expect(result.exitCode).toBe(1); +}); + test('should not reuse fixtures from one file in another one', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.spec.ts': ` diff --git a/tests/playwright-test/timeout.spec.ts b/tests/playwright-test/timeout.spec.ts index 50069b3280..a079c3f70b 100644 --- a/tests/playwright-test/timeout.spec.ts +++ b/tests/playwright-test/timeout.spec.ts @@ -182,7 +182,7 @@ test('should respect fixture timeout', async ({ runInlineTest }) => { slowSetup: [async ({}, use) => { await new Promise(f => setTimeout(f, 2000)); await use('hey'); - }, { timeout: 500, _title: 'custom title' }], + }, { timeout: 500, title: 'custom title' }], slowTeardown: [async ({}, use) => { await use('hey'); await new Promise(f => setTimeout(f, 2000)); @@ -227,7 +227,7 @@ test('should respect test.setTimeout in the worker fixture', async ({ runInlineT slowTeardown: [async ({}, use) => { await use('hey'); await new Promise(f => setTimeout(f, 2000)); - }, { scope: 'worker', timeout: 400, _title: 'custom title' }], + }, { scope: 'worker', timeout: 400, title: 'custom title' }], }); test('test ok', async ({ fixture, noTimeout }) => { await new Promise(f => setTimeout(f, 1000)); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 0367f3259c..80880f71a7 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -140,13 +140,13 @@ export type WorkerFixture = (args: Args, use: (r: R) = type TestFixtureValue = Exclude | TestFixture; type WorkerFixtureValue = Exclude | WorkerFixture; export type Fixtures = { - [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined }]; + [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }]; } & { - [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined }]; + [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }]; } & { - [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined }]; + [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; } & { - [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }]; + [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; }; type BrowserName = 'chromium' | 'firefox' | 'webkit';