From 74976b1da827cd8b7494e869f21db96daadb24a7 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Mon, 24 Jun 2024 21:34:42 +0530 Subject: [PATCH 01/33] docs: Fixes minor typo. Changes `These image` -> `This image` (#31413) Fixes a minor typo in the docker doc in `These image` --- docs/src/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docker.md b/docs/src/docker.md index 11553a578a..0946c2f68b 100644 --- a/docs/src/docker.md +++ b/docs/src/docker.md @@ -5,7 +5,7 @@ title: "Docker" ## Introduction -[Dockerfile.jammy] can be used to run Playwright scripts in Docker environment. These image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately. +[Dockerfile.jammy] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately. ## Usage From 865f0d822138303eee39070d723878585926e7d7 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 24 Jun 2024 11:28:43 -0700 Subject: [PATCH 02/33] 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 03/33] 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 04/33] 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 05/33] 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 06/33] 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 07/33] 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'; From a6b6b243d0a41127fbcdea7edf2b35f2cdcb241e Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Tue, 25 Jun 2024 03:16:36 -0700 Subject: [PATCH 08/33] chore(driver): roll driver to recent Node.js LTS version (#31432) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- utils/build/build-playwright-driver.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh index 975c66446b..c0864681bc 100755 --- a/utils/build/build-playwright-driver.sh +++ b/utils/build/build-playwright-driver.sh @@ -4,7 +4,7 @@ set -x trap "cd $(pwd -P)" EXIT SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" -NODE_VERSION="20.14.0" # autogenerated via ./update-playwright-driver-version.mjs +NODE_VERSION="20.15.0" # autogenerated via ./update-playwright-driver-version.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version") From f11ab2f145299374ea12726b35f260f2a4d084e4 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 25 Jun 2024 19:05:32 +0200 Subject: [PATCH 09/33] chore: enable keepAlive in happy eyeballs http.Agent (#31434) --- packages/playwright-core/src/utils/happy-eyeballs.ts | 5 +++-- tests/library/browsercontext-fetch.spec.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/utils/happy-eyeballs.ts b/packages/playwright-core/src/utils/happy-eyeballs.ts index 4576da7899..37d5c1b271 100644 --- a/packages/playwright-core/src/utils/happy-eyeballs.ts +++ b/packages/playwright-core/src/utils/happy-eyeballs.ts @@ -45,8 +45,9 @@ class HttpsHappyEyeballsAgent extends https.Agent { } } -export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent(); -export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent(); +// These options are aligned with the default Node.js globalAgent options. +export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent({ keepAlive: true }); +export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent({ keepAlive: true }); export async function createSocket(host: string, port: number): Promise { return new Promise((resolve, reject) => { diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 550cc56d74..f8c0327c7d 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -853,7 +853,7 @@ it('should not hang on a brotli encoded Range request', async ({ context, server headers: { range: 'bytes=0-2', }, - })).rejects.toThrow(/(failed to decompress 'br' encoding: Error: unexpected end of file|Parse Error: Data after \`Connection: close\`)/); + })).rejects.toThrow(/Parse Error: Expected HTTP/); }); it('should dispose', async function({ context, server }) { From da441347e24d6136a66baa5beb12b5964b6a6e0f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 25 Jun 2024 10:47:37 -0700 Subject: [PATCH 10/33] fix(runner): do not run beforeEach hooks upon skip modifier (#31426) Fixes https://github.com/microsoft/playwright/issues/31425 --- packages/playwright/src/worker/workerMain.ts | 3 +++ tests/playwright-test/test-modifiers.spec.ts | 22 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index e97bf2945c..2f0dcc424d 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -570,6 +570,9 @@ export class WorkerMain extends ProcessRunner { if (error instanceof TimeoutManagerError) throw error; firstError = firstError ?? error; + // Skip in modifier prevents others from running. + if (error instanceof SkipError) + break; } } if (firstError) diff --git a/tests/playwright-test/test-modifiers.spec.ts b/tests/playwright-test/test-modifiers.spec.ts index 2d11ad0c9a..0dd41bd0ab 100644 --- a/tests/playwright-test/test-modifiers.spec.ts +++ b/tests/playwright-test/test-modifiers.spec.ts @@ -715,3 +715,25 @@ test('should contain only one slow modifier', async ({ runInlineTest }) => { 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' }]); }); + +test('should skip beforeEach hooks upon modifiers', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test } from '@playwright/test'; + test('top', () => {}); + + test.describe(() => { + test.skip(({ viewport }) => true); + test.beforeEach(() => { throw new Error(); }); + + test.describe(() => { + test.beforeEach(() => { throw new Error(); }); + test('test', () => {}); + }); + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.skipped).toBe(1); +}); From dad305478ac91dddbc8ee4dca20460c69f0b19c6 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 26 Jun 2024 14:34:05 +0200 Subject: [PATCH 11/33] docs: remove unnecessary html card (#31444) --- docs/src/locators.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/src/locators.md b/docs/src/locators.md index 6d5926b579..052894a172 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -1444,14 +1444,6 @@ page.getByText("orange").click(); await page.GetByText("orange").ClickAsync(); ``` -```html card -
    -
  • apple
  • -
  • banana
  • -
  • orange
  • -
-``` - #### Filter by text Use the [`method: Locator.filter`] to locate a specific item in a list. From 976373ed2c117c72e1ee068b374baf9e4bd079a6 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 26 Jun 2024 07:51:57 -0700 Subject: [PATCH 12/33] feat(chromium-tip-of-tree): roll to r1234 (#31418) --- packages/playwright-core/browsers.json | 4 ++-- .../src/server/chromium/chromium.ts | 2 +- .../src/server/chromium/crProtocolHelper.ts | 5 +++-- tests/library/capabilities.spec.ts | 17 ++++++++--------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index ae9c7ca714..d5bc5bad36 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1231", + "revision": "1234", "installByDefault": false, - "browserVersion": "128.0.6536.0" + "browserVersion": "128.0.6555.0" }, { "name": "firefox", diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index 6200977473..6a369c918d 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -319,7 +319,7 @@ export class Chromium extends BrowserType { if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW) chromeArguments.push('--headless=new'); else - chromeArguments.push('--headless'); + chromeArguments.push('--headless=old'); chromeArguments.push( '--hide-scrollbars', diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts index 36bc712aa6..9458bed124 100644 --- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts +++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts @@ -37,7 +37,7 @@ export function getExceptionMessage(exceptionDetails: Protocol.Runtime.Exception } export async function releaseObject(client: CRSession, objectId: string) { - await client.send('Runtime.releaseObject', { objectId }).catch(error => {}); + await client.send('Runtime.releaseObject', { objectId }).catch(error => { }); } export async function saveProtocolStream(client: CRSession, handle: string, path: string) { @@ -91,7 +91,8 @@ export function exceptionToError(exceptionDetails: Protocol.Runtime.ExceptionDet const err = new Error(message); err.stack = stack; - err.name = name; + const nameOverride = exceptionDetails.exception?.preview?.properties.find(o => o.name === 'name'); + err.name = nameOverride ? nameOverride.value ?? 'Error' : name; return err; } diff --git a/tests/library/capabilities.spec.ts b/tests/library/capabilities.spec.ts index 66fc9058e9..863a3bafca 100644 --- a/tests/library/capabilities.spec.ts +++ b/tests/library/capabilities.spec.ts @@ -139,15 +139,14 @@ it('should not crash on showDirectoryPicker', async ({ page, server, browserName it.skip(browserName === 'chromium' && browserMajorVersion < 99, 'Fixed in Chromium r956769'); it.skip(browserName !== 'chromium', 'showDirectoryPicker is only available in Chromium'); await page.goto(server.EMPTY_PAGE); - await Promise.race([ - page.evaluate(async () => { - const dir = await (window as any).showDirectoryPicker(); - return dir.name; - }).catch(e => expect(e.message).toContain('DOMException: The user aborted a request')), - // The dialog will not be accepted, so we just wait for some time to - // to give the browser a chance to crash. - new Promise(r => setTimeout(r, 1000)) - ]); + page.evaluate(async () => { + const dir = await (window as any).showDirectoryPicker(); + return dir.name; + // In headless it throws (aborted), in headed it stalls (Test ended) and waits for the picker to be accepted. + }).catch(e => expect(e.message).toMatch(/((DOMException|AbortError): The user aborted a request|Test ended)/)); + // The dialog will not be accepted, so we just wait for some time to + // to give the browser a chance to crash. + await page.waitForTimeout(3_000); }); it('should not crash on storage.getDirectory()', async ({ page, server, browserName, isMac }) => { From 41b185d643c3433d89fbd39ddf9d8c9c0f76c172 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:20:28 -0700 Subject: [PATCH 13/33] feat(webkit): roll to r2038 (#31441) --- packages/playwright-core/browsers.json | 2 +- .../src/server/webkit/protocol.d.ts | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index d5bc5bad36..6e1da4421b 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2037", + "revision": "2038", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts index ba9ba9e5d3..9e71534a17 100644 --- a/packages/playwright-core/src/server/webkit/protocol.d.ts +++ b/packages/playwright-core/src/server/webkit/protocol.d.ts @@ -5013,6 +5013,23 @@ might return multiple quads for inline nodes. * UTC time in seconds, counted from January 1, 1970. */ export type TimeSinceEpoch = number; + /** + * Touch point. + */ + export interface TouchPoint { + /** + * X coordinate of the event relative to the main frame's viewport in CSS pixels. + */ + x: number; + /** + * Y coordinate of the event relative to the main frame's viewport in CSS pixels. + */ + y: number; + /** + * Identifier used to track touch sources between events, must be unique within an event. + */ + id: number; + } /** @@ -5169,6 +5186,26 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the } export type dispatchTapEventReturnValue = { } + /** + * Dispatches a touch event to the page. + */ + export type dispatchTouchEventParameters = { + /** + * Type of the touch event. + */ + type: "touchStart"|"touchMove"|"touchEnd"|"touchCancel"; + /** + * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 +(default: 0). + */ + modifiers?: number; + /** + * List of touch points + */ + touchPoints?: TouchPoint[]; + } + export type dispatchTouchEventReturnValue = { + } } export module Inspector { @@ -9532,6 +9569,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "Input.dispatchMouseEvent": Input.dispatchMouseEventParameters; "Input.dispatchWheelEvent": Input.dispatchWheelEventParameters; "Input.dispatchTapEvent": Input.dispatchTapEventParameters; + "Input.dispatchTouchEvent": Input.dispatchTouchEventParameters; "Inspector.enable": Inspector.enableParameters; "Inspector.disable": Inspector.disableParameters; "Inspector.initialized": Inspector.initializedParameters; @@ -9843,6 +9881,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "Input.dispatchMouseEvent": Input.dispatchMouseEventReturnValue; "Input.dispatchWheelEvent": Input.dispatchWheelEventReturnValue; "Input.dispatchTapEvent": Input.dispatchTapEventReturnValue; + "Input.dispatchTouchEvent": Input.dispatchTouchEventReturnValue; "Inspector.enable": Inspector.enableReturnValue; "Inspector.disable": Inspector.disableReturnValue; "Inspector.initialized": Inspector.initializedReturnValue; From 111876d52619ea662d691f68552ba44ef757224b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 26 Jun 2024 15:39:43 -0700 Subject: [PATCH 14/33] docs: improve addCookies.cookie parameter description (#31456) --- docs/src/api/class-browsercontext.md | 10 +++------- packages/playwright-core/types/types.d.ts | 11 +++++------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 686db16f4a..48542a2603 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -357,18 +357,14 @@ await context.AddCookiesAsync(new[] { cookie1, cookie2 }); - `cookies` <[Array]<[Object]>> - `name` <[string]> - `value` <[string]> - - `url` ?<[string]> either url or domain / path are required. Optional. - - `domain` ?<[string]> either url or domain / path are required Optional. - - `path` ?<[string]> either url or domain / path are required Optional. + - `url` ?<[string]> Either url or domain / path are required. Optional. + - `domain` ?<[string]> For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url or domain / path are required. Optional. + - `path` ?<[string]> Either url or domain / path are required Optional. - `expires` ?<[float]> Unix time in seconds. Optional. - `httpOnly` ?<[boolean]> Optional. - `secure` ?<[boolean]> Optional. - `sameSite` ?<[SameSiteAttribute]<"Strict"|"Lax"|"None">> Optional. -Adds cookies to the browser context. - -For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". - ## async method: BrowserContext.addInitScript * since: v1.8 diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 00b20c80c9..f92eeb71a1 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8286,9 +8286,7 @@ export interface BrowserContext { * await browserContext.addCookies([cookieObject1, cookieObject2]); * ``` * - * @param cookies Adds cookies to the browser context. - * - * For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". + * @param cookies */ addCookies(cookies: ReadonlyArray<{ name: string; @@ -8296,17 +8294,18 @@ export interface BrowserContext { value: string; /** - * either url or domain / path are required. Optional. + * Either url or domain / path are required. Optional. */ url?: string; /** - * either url or domain / path are required Optional. + * For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url + * or domain / path are required. Optional. */ domain?: string; /** - * either url or domain / path are required Optional. + * Either url or domain / path are required Optional. */ path?: string; From 87785d6092bc0022e599a5aed684f03049af4d84 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:01:16 -0700 Subject: [PATCH 15/33] feat(chromium): roll to r1125 (#31467) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 4 +- packages/playwright-core/browsers.json | 4 +- .../src/server/deviceDescriptorsSource.json | 96 +++++++++---------- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 0b354bce3c..e225e4a9ce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-127.0.6533.17-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-127.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-127.0.6533.26-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-127.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 127.0.6533.17 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 127.0.6533.26 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 17.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 127.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 6e1da4421b..9a38e1c62a 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,9 +3,9 @@ "browsers": [ { "name": "chromium", - "revision": "1124", + "revision": "1125", "installByDefault": true, - "browserVersion": "127.0.6533.17" + "browserVersion": "127.0.6533.26" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 450b2f0d39..ee4e655e5a 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -978,7 +978,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -989,7 +989,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1000,7 +1000,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1011,7 +1011,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1022,7 +1022,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1033,7 +1033,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1044,7 +1044,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1055,7 +1055,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1066,7 +1066,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1077,7 +1077,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1088,7 +1088,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1099,7 +1099,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1110,7 +1110,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1121,7 +1121,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1132,7 +1132,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1143,7 +1143,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1154,7 +1154,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1165,7 +1165,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1176,7 +1176,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1187,7 +1187,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1242,7 +1242,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1253,7 +1253,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1264,7 +1264,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1275,7 +1275,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1286,7 +1286,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1297,7 +1297,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1308,7 +1308,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1319,7 +1319,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1330,7 +1330,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1345,7 +1345,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1360,7 +1360,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1375,7 +1375,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1390,7 +1390,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1405,7 +1405,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1420,7 +1420,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1431,7 +1431,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1442,7 +1442,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1457,7 +1457,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36 Edg/127.0.6533.17", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36 Edg/127.0.6533.26", "screen": { "width": 1792, "height": 1120 @@ -1502,7 +1502,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1517,7 +1517,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36 Edg/127.0.6533.17", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36 Edg/127.0.6533.26", "screen": { "width": 1920, "height": 1080 From c9e673c6dca746384338ab6bb0cf63c7e7caa9b2 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 27 Jun 2024 09:29:20 -0700 Subject: [PATCH 16/33] fix(utility): create utility world when web security is disabled (#31458) Reverts previous attempt at #31096 Fixes: https://github.com/microsoft/playwright/issues/31431 Fixes: https://github.com/microsoft/playwright/issues/31442 --- .../src/server/browserContext.ts | 12 ++-- .../src/server/chromium/crBrowser.ts | 6 +- .../src/server/chromium/crPage.ts | 46 ++++++------- .../src/server/firefox/ffBrowser.ts | 6 +- .../src/server/firefox/ffPage.ts | 11 +-- packages/playwright-core/src/server/page.ts | 27 ++++++-- .../src/server/webkit/wkBrowser.ts | 4 +- .../src/server/webkit/wkPage.ts | 8 +-- .../chromium/disable-web-security.spec.ts | 68 +++++++++++++++++++ 9 files changed, 138 insertions(+), 50 deletions(-) create mode 100644 tests/library/chromium/disable-web-security.spec.ts diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index c5a5c9a142..92f3f8a20b 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -24,6 +24,7 @@ import type { Download } from './download'; import type * as frames from './frames'; import { helper } from './helper'; import * as network from './network'; +import { InitScript } from './page'; import type { PageDelegate } from './page'; import { Page, PageBinding } from './page'; import type { Progress, ProgressController } from './progress'; @@ -84,7 +85,7 @@ export abstract class BrowserContext extends SdkObject { private _customCloseHandler?: () => Promise; readonly _tempDirs: string[] = []; private _settingStorageState = false; - readonly initScripts: string[] = []; + readonly initScripts: InitScript[] = []; private _routesInFlight = new Set(); private _debugger!: Debugger; _closeReason: string | undefined; @@ -266,7 +267,7 @@ export abstract class BrowserContext extends SdkObject { protected abstract doGrantPermissions(origin: string, permissions: string[]): Promise; protected abstract doClearPermissions(): Promise; protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise; - protected abstract doAddInitScript(expression: string): Promise; + protected abstract doAddInitScript(initScript: InitScript): Promise; protected abstract doRemoveInitScripts(): Promise; protected abstract doExposeBinding(binding: PageBinding): Promise; protected abstract doRemoveExposedBindings(): Promise; @@ -403,9 +404,10 @@ export abstract class BrowserContext extends SdkObject { this._options.httpCredentials = { username, password: password || '' }; } - async addInitScript(script: string) { - this.initScripts.push(script); - await this.doAddInitScript(script); + async addInitScript(source: string) { + const initScript = new InitScript(source); + this.initScripts.push(initScript); + await this.doAddInitScript(initScript); } async _removeInitScripts(): Promise { diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 254a86c164..777ff2eee8 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -21,7 +21,7 @@ import { Browser } from '../browser'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import { assert, createGuid } from '../../utils'; import * as network from '../network'; -import type { PageBinding, PageDelegate, Worker } from '../page'; +import type { InitScript, PageBinding, PageDelegate, Worker } from '../page'; import { Page } from '../page'; import { Frame } from '../frames'; import type { Dialog } from '../dialog'; @@ -486,9 +486,9 @@ export class CRBrowserContext extends BrowserContext { await (sw as CRServiceWorker).updateHttpCredentials(); } - async doAddInitScript(source: string) { + async doAddInitScript(initScript: InitScript) { for (const page of this.pages()) - await (page._delegate as CRPage).addInitScript(source); + await (page._delegate as CRPage).addInitScript(initScript); } async doRemoveInitScripts() { diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 34d1c53cd4..53b96ad403 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -26,7 +26,7 @@ import * as dom from '../dom'; import * as frames from '../frames'; import { helper } from '../helper'; import * as network from '../network'; -import type { PageBinding, PageDelegate } from '../page'; +import type { InitScript, PageBinding, PageDelegate } from '../page'; import { Page, Worker } from '../page'; import type { Progress } from '../progress'; import type * as types from '../types'; @@ -256,8 +256,8 @@ export class CRPage implements PageDelegate { return this._go(+1); } - async addInitScript(source: string, world: types.World = 'main'): Promise { - await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(source, world)); + async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise { + await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world)); } async removeInitScripts() { @@ -511,6 +511,20 @@ class FrameSession { this._addRendererListeners(); } + const localFrames = this._isMainFrame() ? this._page.frames() : [this._page._frameManager.frame(this._targetId)!]; + for (const frame of localFrames) { + // Note: frames might be removed before we send these. + this._client._sendMayFail('Page.createIsolatedWorld', { + frameId: frame._id, + grantUniveralAccess: true, + worldName: UTILITY_WORLD_NAME, + }); + for (const binding of this._crPage._browserContext._pageBindings.values()) + frame.evaluateExpression(binding.source).catch(e => {}); + for (const initScript of this._crPage._browserContext.initScripts) + frame.evaluateExpression(initScript.source).catch(e => {}); + } + const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':'; if (isInitialEmptyPage) { // Ignore lifecycle events, worlds and bindings for the initial empty page. It is never the final page @@ -520,20 +534,6 @@ class FrameSession { this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event))); }); } else { - const localFrames = this._isMainFrame() ? this._page.frames() : [this._page._frameManager.frame(this._targetId)!]; - for (const frame of localFrames) { - // Note: frames might be removed before we send these. - this._client._sendMayFail('Page.createIsolatedWorld', { - frameId: frame._id, - grantUniveralAccess: true, - worldName: UTILITY_WORLD_NAME, - }); - for (const binding of this._crPage._browserContext._pageBindings.values()) - frame.evaluateExpression(binding.source).catch(e => {}); - for (const source of this._crPage._browserContext.initScripts) - frame.evaluateExpression(source).catch(e => {}); - } - this._firstNonInitialNavigationCommittedFulfill(); this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event))); } @@ -575,10 +575,10 @@ class FrameSession { promises.push(this._updateFileChooserInterception(true)); for (const binding of this._crPage._page.allBindings()) promises.push(this._initBinding(binding)); - for (const source of this._crPage._browserContext.initScripts) - promises.push(this._evaluateOnNewDocument(source, 'main')); - for (const source of this._crPage._page.initScripts) - promises.push(this._evaluateOnNewDocument(source, 'main')); + for (const initScript of this._crPage._browserContext.initScripts) + promises.push(this._evaluateOnNewDocument(initScript, 'main')); + for (const initScript of this._crPage._page.initScripts) + promises.push(this._evaluateOnNewDocument(initScript, 'main')); if (screencastOptions) promises.push(this._startVideoRecording(screencastOptions)); } @@ -1099,9 +1099,9 @@ class FrameSession { await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed. } - async _evaluateOnNewDocument(source: string, world: types.World): Promise { + async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise { const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined; - const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source, worldName }); + const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName }); this._evaluateOnNewDocumentIdentifiers.push(identifier); } diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index ae26d0994a..7ed2d8cb2f 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -21,7 +21,7 @@ import type { BrowserOptions } from '../browser'; import { Browser } from '../browser'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import * as network from '../network'; -import type { Page, PageBinding, PageDelegate } from '../page'; +import type { InitScript, Page, PageBinding, PageDelegate } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; import type * as channels from '@protocol/channels'; @@ -352,8 +352,8 @@ export class FFBrowserContext extends BrowserContext { await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials }); } - async doAddInitScript(source: string) { - await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script })) }); + async doAddInitScript(initScript: InitScript) { + await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script: script.source })) }); } async doRemoveInitScripts() { diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 77fcd480bb..aac22d5e50 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -21,6 +21,7 @@ import type * as frames from '../frames'; import type { RegisteredListener } from '../../utils/eventsHelper'; import { eventsHelper } from '../../utils/eventsHelper'; import type { PageBinding, PageDelegate } from '../page'; +import { InitScript } from '../page'; import { Page, Worker } from '../page'; import type * as types from '../types'; import { getAccessibilityTree } from './ffAccessibility'; @@ -56,7 +57,7 @@ export class FFPage implements PageDelegate { private _eventListeners: RegisteredListener[]; private _workers = new Map(); private _screencastId: string | undefined; - private _initScripts: { script: string, worldName?: string }[] = []; + private _initScripts: { initScript: InitScript, worldName?: string }[] = []; constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) { this._session = session; @@ -113,7 +114,7 @@ export class FFPage implements PageDelegate { }); // Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy. // Therefore, we can end up with an initialized page without utility world, although very unlikely. - this.addInitScript('', UTILITY_WORLD_NAME).catch(e => this._markAsError(e)); + this.addInitScript(new InitScript(''), UTILITY_WORLD_NAME).catch(e => this._markAsError(e)); } potentiallyUninitializedPage(): Page { @@ -406,9 +407,9 @@ export class FFPage implements PageDelegate { return success; } - async addInitScript(script: string, worldName?: string): Promise { - this._initScripts.push({ script, worldName }); - await this._session.send('Page.setInitScripts', { scripts: this._initScripts }); + async addInitScript(initScript: InitScript, worldName?: string): Promise { + this._initScripts.push({ initScript, worldName }); + await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) }); } async removeInitScripts() { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 28ff203a3c..da9063821c 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -31,7 +31,7 @@ import * as accessibility from './accessibility'; import { FileChooser } from './fileChooser'; import type { Progress } from './progress'; import { ProgressController } from './progress'; -import { LongStandingScope, assert, isError } from '../utils'; +import { LongStandingScope, assert, createGuid, isError } from '../utils'; import { ManualPromise } from '../utils/manualPromise'; import { debugLogger } from '../utils/debugLogger'; import type { ImageComparatorOptions } from '../utils/comparators'; @@ -56,7 +56,7 @@ export interface PageDelegate { goForward(): Promise; exposeBinding(binding: PageBinding): Promise; removeExposedBindings(): Promise; - addInitScript(source: string): Promise; + addInitScript(initScript: InitScript): Promise; removeInitScripts(): Promise; closePage(runBeforeUnload: boolean): Promise; potentiallyUninitializedPage(): Page; @@ -154,7 +154,7 @@ export class Page extends SdkObject { private _emulatedMedia: Partial = {}; private _interceptFileChooser = false; private readonly _pageBindings = new Map(); - readonly initScripts: string[] = []; + readonly initScripts: InitScript[] = []; readonly _screenshotter: Screenshotter; readonly _frameManager: frames.FrameManager; readonly accessibility: accessibility.Accessibility; @@ -527,8 +527,9 @@ export class Page extends SdkObject { } async addInitScript(source: string) { - this.initScripts.push(source); - await this._delegate.addInitScript(source); + const initScript = new InitScript(source); + this.initScripts.push(initScript); + await this._delegate.addInitScript(initScript); } async _removeInitScripts() { @@ -905,6 +906,22 @@ function addPageBinding(bindingName: string, needsHandle: boolean, utilityScript (globalThis as any)[bindingName].__installed = true; } +export class InitScript { + readonly source: string; + + constructor(source: string) { + const guid = createGuid(); + this.source = `(() => { + globalThis.__pwInitScripts = globalThis.__pwInitScripts || {}; + const hasInitScript = globalThis.__pwInitScripts[${JSON.stringify(guid)}]; + if (hasInitScript) + return; + globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true; + ${source} + })();`; + } +} + class FrameThrottler { private _acks: (() => void)[] = []; private _defaultInterval: number; diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index a8541e73cf..9eedec410b 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -22,7 +22,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper'; import { assert } from '../../utils'; import { eventsHelper } from '../../utils/eventsHelper'; import * as network from '../network'; -import type { Page, PageBinding, PageDelegate } from '../page'; +import type { InitScript, Page, PageBinding, PageDelegate } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; import type * as channels from '@protocol/channels'; @@ -315,7 +315,7 @@ export class WKBrowserContext extends BrowserContext { await (page._delegate as WKPage).updateHttpCredentials(); } - async doAddInitScript(source: string) { + async doAddInitScript(initScript: InitScript) { for (const page of this.pages()) await (page._delegate as WKPage)._updateBootstrapScript(); } diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 9507f187e5..adfd06c05e 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -30,7 +30,7 @@ import { eventsHelper } from '../../utils/eventsHelper'; import { helper } from '../helper'; import type { JSHandle } from '../javascript'; import * as network from '../network'; -import type { PageBinding, PageDelegate } from '../page'; +import type { InitScript, PageBinding, PageDelegate } from '../page'; import { Page } from '../page'; import type { Progress } from '../progress'; import type * as types from '../types'; @@ -777,7 +777,7 @@ export class WKPage implements PageDelegate { await this._updateBootstrapScript(); } - async addInitScript(script: string): Promise { + async addInitScript(initScript: InitScript): Promise { await this._updateBootstrapScript(); } @@ -797,8 +797,8 @@ export class WKPage implements PageDelegate { for (const binding of this._page.allBindings()) scripts.push(binding.source); - scripts.push(...this._browserContext.initScripts); - scripts.push(...this._page.initScripts); + scripts.push(...this._browserContext.initScripts.map(s => s.source)); + scripts.push(...this._page.initScripts.map(s => s.source)); return scripts.join(';\n'); } diff --git a/tests/library/chromium/disable-web-security.spec.ts b/tests/library/chromium/disable-web-security.spec.ts new file mode 100644 index 0000000000..fab5599a76 --- /dev/null +++ b/tests/library/chromium/disable-web-security.spec.ts @@ -0,0 +1,68 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { contextTest as it, expect } from '../../config/browserTest'; + +it.use({ + launchOptions: async ({ launchOptions }, use) => { + await use({ ...launchOptions, args: ['--disable-web-security'] }); + } +}); + +it('test utility world in popup w/ --disable-web-security', async ({ page, server }) => { + server.setRoute('/main.html', (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/html' + }); + res.end(`Click me`); + }); + server.setRoute('/target.html', (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/html' + }); + res.end(``); + }); + + await page.goto(server.PREFIX + '/main.html'); + const page1Promise = page.context().waitForEvent('page'); + await page.getByRole('link', { name: 'Click me' }).click(); + const page1 = await page1Promise; + await expect(page1).toHaveURL(/target/); +}); + +it('test init script w/ --disable-web-security', async ({ page, server }) => { + server.setRoute('/main.html', (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/html' + }); + res.end(`Click me`); + }); + server.setRoute('/target.html', (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/html' + }); + res.end(``); + }); + + await page.context().addInitScript('window.injected = 123'); + await page.goto(server.PREFIX + '/main.html'); + const page1Promise = page.context().waitForEvent('page'); + await page.getByRole('link', { name: 'Click me' }).click(); + const page1 = await page1Promise; + const value = await page1.evaluate('window.injected'); + expect(value).toBe(123); +}); From 33ac75b7ab1492894fccb2bb8c2ff612a13cd685 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:07:49 -0700 Subject: [PATCH 17/33] feat(chromium-tip-of-tree): roll to r1236 (#31466) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 9a38e1c62a..a0e5cfade1 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1234", + "revision": "1236", "installByDefault": false, - "browserVersion": "128.0.6555.0" + "browserVersion": "128.0.6561.0" }, { "name": "firefox", From a3e31fd2c4146e67a63ff4d3dc4f655533adcbde Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 27 Jun 2024 14:37:36 -0700 Subject: [PATCH 18/33] feat: introduce touchscreen.touch() for dispatching raw touch events (#31457) --- docs/src/api/class-touchscreen.md | 20 +++++ packages/playwright-core/src/client/input.ts | 4 + .../playwright-core/src/protocol/debug.ts | 2 + .../playwright-core/src/protocol/validator.ts | 9 +++ .../src/server/chromium/crInput.ts | 15 ++++ .../src/server/dispatchers/pageDispatcher.ts | 4 + .../src/server/firefox/ffInput.ts | 4 + packages/playwright-core/src/server/input.ts | 16 ++++ .../src/server/webkit/wkInput.ts | 16 ++++ packages/playwright-core/types/types.d.ts | 23 ++++++ packages/protocol/src/channels.ts | 13 ++++ packages/protocol/src/protocol.yml | 21 +++++ tests/library/touch.spec.ts | 76 +++++++++++++++++++ 13 files changed, 223 insertions(+) create mode 100644 tests/library/touch.spec.ts diff --git a/docs/src/api/class-touchscreen.md b/docs/src/api/class-touchscreen.md index 90ea8cd759..79fd134dca 100644 --- a/docs/src/api/class-touchscreen.md +++ b/docs/src/api/class-touchscreen.md @@ -20,3 +20,23 @@ Dispatches a `touchstart` and `touchend` event with a single touch at the positi ### param: Touchscreen.tap.y * since: v1.8 - `y` <[float]> + +## async method: Touchscreen.touch +* since: v1.46 + +Synthesizes a touch event. + +### param: Touchscreen.touch.type +* since: v1.46 +- `type` <[TouchType]<"touchstart"|"touchend"|"touchmove"|"touchcancel">> + +Type of the touch event. + +### param: Touchscreen.touch.touches +* since: v1.46 +- `touchPoints` <[Array]<[Object]>> + - `x` <[float]> x coordinate of the event in CSS pixels. + - `y` <[float]> y coordinate of the event in CSS pixels. + - `id` ?<[int]> Identifier used to track the touch point between events, must be unique within an event. Optional. + +List of touch points for this event. `id` is a unique identifier of a touch point that helps identify it between touch events for the duration of its movement around the surface. \ No newline at end of file diff --git a/packages/playwright-core/src/client/input.ts b/packages/playwright-core/src/client/input.ts index e06b0e3e4a..0cc841619f 100644 --- a/packages/playwright-core/src/client/input.ts +++ b/packages/playwright-core/src/client/input.ts @@ -89,4 +89,8 @@ export class Touchscreen implements api.Touchscreen { async tap(x: number, y: number) { await this._page._channel.touchscreenTap({ x, y }); } + + async touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[]) { + await this._page._channel.touchscreenTouch({ type, touchPoints }); + } } diff --git a/packages/playwright-core/src/protocol/debug.ts b/packages/playwright-core/src/protocol/debug.ts index 4f44f5941e..f8e1f6c4b2 100644 --- a/packages/playwright-core/src/protocol/debug.ts +++ b/packages/playwright-core/src/protocol/debug.ts @@ -31,6 +31,7 @@ export const slowMoActions = new Set([ 'Page.mouseClick', 'Page.mouseWheel', 'Page.touchscreenTap', + 'Page.touchscreenTouch', 'Frame.blur', 'Frame.check', 'Frame.click', @@ -89,6 +90,7 @@ export const commandsWithTracingSnapshots = new Set([ 'Page.mouseClick', 'Page.mouseWheel', 'Page.touchscreenTap', + 'Page.touchscreenTouch', 'Frame.evalOnSelector', 'Frame.evalOnSelectorAll', 'Frame.addScriptTag', diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 9187dc13d2..63679d1993 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1237,6 +1237,15 @@ scheme.PageTouchscreenTapParams = tObject({ y: tNumber, }); scheme.PageTouchscreenTapResult = tOptional(tObject({})); +scheme.PageTouchscreenTouchParams = tObject({ + type: tEnum(['touchstart', 'touchend', 'touchmove', 'touchcancel']), + touchPoints: tArray(tObject({ + x: tNumber, + y: tNumber, + id: tOptional(tNumber), + })), +}); +scheme.PageTouchscreenTouchResult = tOptional(tObject({})); scheme.PageAccessibilitySnapshotParams = tObject({ interestingOnly: tOptional(tBoolean), root: tOptional(tChannel(['ElementHandle'])), diff --git a/packages/playwright-core/src/server/chromium/crInput.ts b/packages/playwright-core/src/server/chromium/crInput.ts index bbfd973d10..47e7cc53e9 100644 --- a/packages/playwright-core/src/server/chromium/crInput.ts +++ b/packages/playwright-core/src/server/chromium/crInput.ts @@ -179,4 +179,19 @@ export class RawTouchscreenImpl implements input.RawTouchscreen { }), ]); } + async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set) { + let type: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel'; + switch (eventType) { + case 'touchstart': type = 'touchStart'; break; + case 'touchmove': type = 'touchMove'; break; + case 'touchend': type = 'touchEnd'; break; + case 'touchcancel': type = 'touchCancel'; break; + default: throw new Error('Invalid eventType: ' + eventType); + } + await this._client.send('Input.dispatchTouchEvent', { + type, + touchPoints, + modifiers: toModifiersMask(modifiers) + }); + } } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 3101cd051d..9acc7378b2 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -265,6 +265,10 @@ export class PageDispatcher extends Dispatcher { const rootAXNode = await this._page.accessibility.snapshot({ interestingOnly: params.interestingOnly, diff --git a/packages/playwright-core/src/server/firefox/ffInput.ts b/packages/playwright-core/src/server/firefox/ffInput.ts index 66f35399a5..fcf53ee9fc 100644 --- a/packages/playwright-core/src/server/firefox/ffInput.ts +++ b/packages/playwright-core/src/server/firefox/ffInput.ts @@ -166,4 +166,8 @@ export class RawTouchscreenImpl implements input.RawTouchscreen { modifiers: toModifiersMask(modifiers), }); } + + async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set) { + throw new Error('Not implemented yet.'); + } } diff --git a/packages/playwright-core/src/server/input.ts b/packages/playwright-core/src/server/input.ts index f09f91b86f..501874322c 100644 --- a/packages/playwright-core/src/server/input.ts +++ b/packages/playwright-core/src/server/input.ts @@ -308,6 +308,7 @@ function buildLayoutClosure(layout: keyboardLayout.KeyboardLayout): Map): Promise; + touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set): Promise; } export class Touchscreen { @@ -326,4 +327,19 @@ export class Touchscreen { throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.'); await this._raw.tap(x, y, this._page.keyboard._modifiers()); } + async touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], metadata?: CallMetadata) { + if (metadata && touchPoints.length === 1) + metadata.point = { x: touchPoints[0].x, y: touchPoints[0].y }; + if (!this._page._browserContext._options.hasTouch) + throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.'); + const ids = new Set(); + for (const point of touchPoints) { + if (point.id !== undefined) { + if (ids.has(point.id)) + throw new Error(`Duplicate touch point id: ${point.id}`); + ids.add(point.id); + } + } + await this._raw.touch(type, touchPoints, this._page.keyboard._modifiers()); + } } diff --git a/packages/playwright-core/src/server/webkit/wkInput.ts b/packages/playwright-core/src/server/webkit/wkInput.ts index 0732f246d1..88afa774c8 100644 --- a/packages/playwright-core/src/server/webkit/wkInput.ts +++ b/packages/playwright-core/src/server/webkit/wkInput.ts @@ -182,4 +182,20 @@ export class RawTouchscreenImpl implements input.RawTouchscreen { modifiers: toModifiersMask(modifiers), }); } + + async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set) { + let type: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel'; + switch (eventType) { + case 'touchstart': type = 'touchStart'; break; + case 'touchmove': type = 'touchMove'; break; + case 'touchend': type = 'touchEnd'; break; + case 'touchcancel': type = 'touchCancel'; break; + default: throw new Error('Invalid eventType: ' + eventType); + } + await this._pageProxySession.send('Input.dispatchTouchEvent', { + type, + touchPoints: touchPoints.map(p => ({ ...p, id: p.id || 0 })), + modifiers: toModifiersMask(modifiers) + }); + } } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index f92eeb71a1..3152c067a3 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -19682,6 +19682,29 @@ export interface Touchscreen { * @param y */ tap(x: number, y: number): Promise; + + /** + * Synthesizes a touch event. + * @param type Type of the touch event. + * @param touchPoints List of touch points for this event. `id` is a unique identifier of a touch point that helps identify it between + * touch events for the duration of its movement around the surface. + */ + touch(type: "touchstart"|"touchend"|"touchmove"|"touchcancel", touchPoints: ReadonlyArray<{ + /** + * x coordinate of the event in CSS pixels. + */ + x: number; + + /** + * y coordinate of the event in CSS pixels. + */ + y: number; + + /** + * Identifier used to track the touch point between events, must be unique within an event. Optional. + */ + id?: number; + }>): Promise; } /** diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index ffb591556c..a1ae5a13e0 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1890,6 +1890,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { mouseClick(params: PageMouseClickParams, metadata?: CallMetadata): Promise; mouseWheel(params: PageMouseWheelParams, metadata?: CallMetadata): Promise; touchscreenTap(params: PageTouchscreenTapParams, metadata?: CallMetadata): Promise; + touchscreenTouch(params: PageTouchscreenTouchParams, metadata?: CallMetadata): Promise; accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: CallMetadata): Promise; pdf(params: PagePdfParams, metadata?: CallMetadata): Promise; startJSCoverage(params: PageStartJSCoverageParams, metadata?: CallMetadata): Promise; @@ -2257,6 +2258,18 @@ export type PageTouchscreenTapOptions = { }; export type PageTouchscreenTapResult = void; +export type PageTouchscreenTouchParams = { + type: 'touchstart' | 'touchend' | 'touchmove' | 'touchcancel', + touchPoints: { + x: number, + y: number, + id?: number, + }[], +}; +export type PageTouchscreenTouchOptions = { + +}; +export type PageTouchscreenTouchResult = void; export type PageAccessibilitySnapshotParams = { interestingOnly?: boolean, root?: ElementHandleChannel, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 14799ef17e..54f356102b 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1603,6 +1603,27 @@ Page: slowMo: true snapshot: true + touchscreenTouch: + parameters: + type: + type: enum + literals: + - touchstart + - touchend + - touchmove + - touchcancel + touchPoints: + type: array + items: + type: object + properties: + x: number + y: number + id: number? + flags: + slowMo: true + snapshot: true + accessibilitySnapshot: parameters: interestingOnly: boolean? diff --git a/tests/library/touch.spec.ts b/tests/library/touch.spec.ts new file mode 100644 index 0000000000..f842beb207 --- /dev/null +++ b/tests/library/touch.spec.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { contextTest as it, expect } from '../config/browserTest'; +import type { Locator } from 'playwright-core'; + +it.use({ hasTouch: true }); + +it.fixme(({ browserName }) => browserName === 'firefox'); + +it('slow swipe events @smoke', async ({ page }) => { + it.fixme(); + await page.setContent(`
a
`); + const eventsHandle = await trackEvents(await page.locator('#a')); + const center = await centerPoint(page.locator('#a')); + await page.touchscreen.touch('touchstart', [{ ...center, id: 1 }]); + expect.soft(await eventsHandle.jsonValue()).toEqual([ + 'pointerover', + 'pointerenter', + 'pointerdown', + 'touchstart', + ]); + + await eventsHandle.evaluate(events => events.length = 0); + await page.touchscreen.touch('touchmove', [{ x: center.x + 10, y: center.y + 10, id: 1 }]); + await page.touchscreen.touch('touchmove', [{ x: center.x + 20, y: center.y + 20, id: 1 }]); + expect.soft(await eventsHandle.jsonValue()).toEqual([ + 'pointermove', + 'touchmove', + 'pointermove', + 'touchmove', + ]); + + await eventsHandle.evaluate(events => events.length = 0); + await page.touchscreen.touch('touchend', [{ x: center.x + 20, y: center.y + 20, id: 1 }]); + expect.soft(await eventsHandle.jsonValue()).toEqual([ + 'pointerup', + 'pointerout', + 'pointerleave', + 'touchend', + ]); +}); + + +async function trackEvents(target: Locator) { + const eventsHandle = await target.evaluateHandle(target => { + const events: string[] = []; + for (const event of [ + 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'click', + 'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover', 'pointerup', + 'touchstart', 'touchend', 'touchmove', 'touchcancel',]) + target.addEventListener(event, () => events.push(event), { passive: false }); + return events; + }); + return eventsHandle; +} + +async function centerPoint(e: Locator) { + const box = await e.boundingBox(); + if (!box) + throw new Error('Element is not visible'); + return { x: box.x + box.width / 2, y: box.y + box.height / 2 }; +} \ No newline at end of file From f1b04aaaf451a37534be8561caa2aa4d743ba779 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 28 Jun 2024 06:23:38 -0700 Subject: [PATCH 19/33] feat(webkit): roll to r2039 (#31480) --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index a0e5cfade1..761a95ce6b 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2038", + "revision": "2039", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 9bc45ea2fc69ec1109581d36c6a5a4cf2300ee9f Mon Sep 17 00:00:00 2001 From: Rui Figueira Date: Fri, 28 Jun 2024 18:36:11 +0100 Subject: [PATCH 20/33] feature(trace-viewer): embedded mode support PoC (#30885) Companion PR of https://github.com/microsoft/playwright-vscode/pull/483 --- packages/trace-viewer/embedded.html | 27 ++++++ packages/trace-viewer/src/embedded.tsx | 61 ++++++++++++ .../src/ui/embeddedWorkbenchLoader.css | 68 +++++++++++++ .../src/ui/embeddedWorkbenchLoader.tsx | 96 +++++++++++++++++++ packages/trace-viewer/src/ui/snapshotTab.tsx | 7 +- packages/trace-viewer/src/ui/workbench.tsx | 6 +- packages/trace-viewer/vite.config.ts | 1 + 7 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 packages/trace-viewer/embedded.html create mode 100644 packages/trace-viewer/src/embedded.tsx create mode 100644 packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css create mode 100644 packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx diff --git a/packages/trace-viewer/embedded.html b/packages/trace-viewer/embedded.html new file mode 100644 index 0000000000..7d0fd2f175 --- /dev/null +++ b/packages/trace-viewer/embedded.html @@ -0,0 +1,27 @@ + + + + + + + Playwright Trace Viewer for VS Code + + +
+ + + diff --git a/packages/trace-viewer/src/embedded.tsx b/packages/trace-viewer/src/embedded.tsx new file mode 100644 index 0000000000..8f0a09e560 --- /dev/null +++ b/packages/trace-viewer/src/embedded.tsx @@ -0,0 +1,61 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@web/common.css'; +import { applyTheme } from '@web/theme'; +import '@web/third_party/vscode/codicon.css'; +import React from 'react'; +import * as ReactDOM from 'react-dom'; +import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader'; + +(async () => { + applyTheme(); + + // workaround to send keystrokes back to vscode webview to keep triggering key bindings there + const handleKeyEvent = (e: KeyboardEvent) => { + if (!e.isTrusted) + return; + window.parent?.postMessage({ + type: e.type, + key: e.key, + keyCode: e.keyCode, + code: e.code, + shiftKey: e.shiftKey, + altKey: e.altKey, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + repeat: e.repeat, + }, '*'); + }; + window.addEventListener('keydown', handleKeyEvent); + window.addEventListener('keyup', handleKeyEvent); + + if (window.location.protocol !== 'file:') { + if (!navigator.serviceWorker) + throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`); + navigator.serviceWorker.register('sw.bundle.js'); + if (!navigator.serviceWorker.controller) { + await new Promise(f => { + navigator.serviceWorker.oncontrollerchange = () => f(); + }); + } + + // Keep SW running. + setInterval(function() { fetch('ping'); }, 10000); + } + + ReactDOM.render(, document.querySelector('#root')); +})(); diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css new file mode 100644 index 0000000000..2274355322 --- /dev/null +++ b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css @@ -0,0 +1,68 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.empty-state { + display: flex; + align-items: center; + justify-content: center; + flex: auto; + flex-direction: column; + background-color: var(--vscode-editor-background); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 100; + line-height: 24px; +} + +body .empty-state { + background: rgba(255, 255, 255, 0.8); +} + +body.dark-mode .empty-state { + background: rgba(0, 0, 0, 0.8); +} + +.empty-state .title { + font-size: 24px; + font-weight: bold; + margin-bottom: 30px; +} + +.progress { + flex: none; + width: 100%; + height: 3px; + z-index: 10; +} + +.inner-progress { + background-color: var(--vscode-progressBar-background); + height: 100%; +} + +.workbench-loader { + contain: size; +} + +/* Limit to a reasonable minimum viewport */ +html, body { + min-width: 550px; + min-height: 450px; + overflow: auto; +} diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx new file mode 100644 index 0000000000..17a2211c41 --- /dev/null +++ b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx @@ -0,0 +1,96 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import * as React from 'react'; +import type { ContextEntry } from '../entries'; +import { MultiTraceModel } from './modelUtil'; +import './embeddedWorkbenchLoader.css'; +import { Workbench } from './workbench'; +import { currentTheme, toggleTheme } from '@web/theme'; + +function openPage(url: string, target?: string) { + if (url) + window.parent!.postMessage({ command: 'openExternal', params: { url, target } }, '*'); +} + +export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => { + const [traceURLs, setTraceURLs] = React.useState([]); + const [model, setModel] = React.useState(emptyModel); + const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); + const [processingErrorMessage, setProcessingErrorMessage] = React.useState(null); + + React.useEffect(() => { + window.addEventListener('message', async ({ data: { method, params } }) => { + if (method === 'loadTraceRequested') { + setTraceURLs(params.traceUrl ? [params.traceUrl] : []); + setProcessingErrorMessage(null); + } else if (method === 'applyTheme') { + if (currentTheme() !== params.theme) + toggleTheme(); + } + }); + // notify vscode that it is now listening to its messages + window.parent!.postMessage({ type: 'loaded' }, '*'); + }, []); + + React.useEffect(() => { + (async () => { + if (traceURLs.length) { + const swListener = (event: any) => { + if (event.data.method === 'progress') + setProgress(event.data.params); + }; + navigator.serviceWorker.addEventListener('message', swListener); + setProgress({ done: 0, total: 1 }); + const contextEntries: ContextEntry[] = []; + for (let i = 0; i < traceURLs.length; i++) { + const url = traceURLs[i]; + const params = new URLSearchParams(); + params.set('trace', url); + const response = await fetch(`contexts?${params.toString()}`); + if (!response.ok) { + setProcessingErrorMessage((await response.json()).error); + return; + } + contextEntries.push(...(await response.json())); + } + navigator.serviceWorker.removeEventListener('message', swListener); + const model = new MultiTraceModel(contextEntries); + setProgress({ done: 0, total: 0 }); + setModel(model); + } else { + setModel(emptyModel); + } + })(); + }, [traceURLs]); + + React.useEffect(() => { + if (processingErrorMessage) + window.parent?.postMessage({ method: 'showErrorMessage', params: { message: processingErrorMessage } }, '*'); + }, [processingErrorMessage]); + + return
+
+
+
+ + {!traceURLs.length &&
+
Select test to see the trace
+
} +
; +}; + +export const emptyModel = new MultiTraceModel([]); diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index bdb955c13c..4bc2abc9eb 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -38,7 +38,8 @@ export const SnapshotTab: React.FunctionComponent<{ setIsInspecting: (isInspecting: boolean) => void, highlightedLocator: string, setHighlightedLocator: (locator: string) => void, -}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => { + openPage?: (url: string, target?: string) => Window | any, +}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => { const [measure, ref] = useMeasure(); const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action'); @@ -190,7 +191,9 @@ export const SnapshotTab: React.FunctionComponent<{ })}
{ - const win = window.open(popoutUrl || '', '_blank'); + if (!openPage) + openPage = window.open; + const win = openPage(popoutUrl || '', '_blank'); win?.addEventListener('DOMContentLoaded', () => { const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []); new ConsoleAPI(injectedScript); diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index c50b345b75..afc003c674 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -51,7 +51,8 @@ export const Workbench: React.FunctionComponent<{ isLive?: boolean, status?: UITestStatus, inert?: boolean, -}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert }) => { + openPage?: (url: string, target?: string) => Window | any, +}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert, openPage }) => { const [selectedAction, setSelectedActionImpl] = React.useState(undefined); const [revealedStack, setRevealedStack] = React.useState(undefined); const [highlightedAction, setHighlightedAction] = React.useState(); @@ -234,7 +235,8 @@ export const Workbench: React.FunctionComponent<{ isInspecting={isInspecting} setIsInspecting={setIsInspecting} highlightedLocator={highlightedLocator} - setHighlightedLocator={locatorPicked} /> + setHighlightedLocator={locatorPicked} + openPage={openPage} /> Date: Fri, 28 Jun 2024 20:04:31 +0200 Subject: [PATCH 21/33] docs: add release video (#31482) --- docs/src/clock.md | 8 ++++++++ docs/src/release-notes-js.md | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/docs/src/clock.md b/docs/src/clock.md index 86d0ad9a10..9fb748f441 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -2,6 +2,7 @@ id: clock title: "Clock" --- +import LiteYouTube from '@site/src/components/LiteYouTube'; ## Introduction @@ -353,3 +354,10 @@ await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); await Page.Clock.RunForAsync(2000); await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM"); ``` + +## Related Videos + + diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 839a4ab07b..5bcea8962b 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube'; ## 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: From f46ae15500df6776b2b8ee29ddca37007e27f621 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 28 Jun 2024 11:46:28 -0700 Subject: [PATCH 22/33] test(clock): fix clock mode bots (#31472) --- packages/playwright-core/src/client/browser.ts | 10 ---------- packages/playwright/src/index.ts | 12 ++++++++++++ tests/page/page-clock.frozen.spec.ts | 2 +- tests/playwright-test/reporter-html.spec.ts | 1 + 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 7c353656cb..be47ddeb51 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -85,16 +85,6 @@ export class Browser extends ChannelOwner implements ap const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions); const context = BrowserContext.from(response.context); await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger); - if (!forReuse && process.env.PW_CLOCK === 'frozen') { - await this._wrapApiCall(async () => { - await context.clock.install({ time: 0 }); - await context.clock.pauseAt(1000); - }, true); - } else if (!forReuse && process.env.PW_CLOCK === 'realtime') { - await this._wrapApiCall(async () => { - await context.clock.install({ time: 0 }); - }, true); - } return context; } diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 2e1236d8ea..a258cbcad5 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -274,6 +274,18 @@ const playwrightFixtures: Fixtures = ({ contexts.set(context, contextData); if (captureVideo) context.on('page', page => contextData.pagesWithVideo.push(page)); + + if (process.env.PW_CLOCK === 'frozen') { + await (context as any)._wrapApiCall(async () => { + await context.clock.install({ time: 0 }); + await context.clock.pauseAt(1000); + }, true); + } else if (process.env.PW_CLOCK === 'realtime') { + await (context as any)._wrapApiCall(async () => { + await context.clock.install({ time: 0 }); + }, true); + } + return context; }); diff --git a/tests/page/page-clock.frozen.spec.ts b/tests/page/page-clock.frozen.spec.ts index 9b70936377..b9b319cd63 100644 --- a/tests/page/page-clock.frozen.spec.ts +++ b/tests/page/page-clock.frozen.spec.ts @@ -23,5 +23,5 @@ it('clock should be frozen', async ({ page }) => { it('clock should be realtime', async ({ page }) => { it.skip(process.env.PW_CLOCK !== 'realtime'); - expect(await page.evaluate('Date.now()')).toBeLessThan(1000); + expect(await page.evaluate('Date.now()')).toBeLessThan(10000); }); diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 83992c0bad..e416cd05c1 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -294,6 +294,7 @@ for (const useIntermediateMergeReport of [false] as const) { }); test('should include image diff when screenshot failed to generate due to animation', async ({ runInlineTest, page, showReport }) => { + test.skip(process.env.PW_CLOCK === 'frozen', 'Assumes Date.now() changes'); const result = await runInlineTest({ 'playwright.config.ts': ` module.exports = { use: { viewport: { width: 200, height: 200 }} }; From ea33137a0e26f006265c2f410bfb037c46d378dc Mon Sep 17 00:00:00 2001 From: Debbie O'Brien Date: Fri, 28 Jun 2024 22:04:39 +0200 Subject: [PATCH 23/33] docs: improve clock guide (#31487) --- docs/src/clock.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/src/clock.md b/docs/src/clock.md index 9fb748f441..ec3aaf8bd5 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -8,6 +8,18 @@ import LiteYouTube from '@site/src/components/LiteYouTube'; Accurately simulating time-dependent behavior is essential for verifying the correctness of applications. Utilizing [Clock] functionality allows developers to manipulate and control time within tests, enabling the precise validation of features such as rendering time, timeouts, scheduled tasks without the delays and variability of real-time execution. +The [Clock] API provides the following methods to control time: +- `setFixedTime`: Sets the fixed time for `Date.now()` and `new Date()`. +- `install`: initializes the clock and allows you to: + - `pauseAt`: Pauses the time at a specific time. + - `fastForward`: Fast forwards the time. + - `runFor`: Runs the time for a specific duration. + - `resume`: Resumes the time. +- `setSystemTime`: Sets the current system time. + +The recommended approach is to use `setFixedTime` to set the time to a specific value. If that doesn't work for your use case, you can use `install` which allows you to pause time later on, fast forward it, tick it, etc. `setSystemTime` is only recommended for advanced use cases. + +:::note [`property: Page.clock`] overrides native global classes and functions related to time allowing them to be manually controlled: - `Date` - `setTimeout` @@ -19,6 +31,7 @@ Accurately simulating time-dependent behavior is essential for verifying the cor - `requestIdleCallback` - `cancelIdleCallback` - `performance` +::: ## Test with predefined time From 4089f4593b3430dda5e4aca28119c50b71bd85e7 Mon Sep 17 00:00:00 2001 From: 4ydx Date: Sat, 29 Jun 2024 05:04:59 +0900 Subject: [PATCH 24/33] fix(codgen): assertValue works with disabled select (#31315) --- .../src/server/injected/recorder/recorder.ts | 4 ++-- tests/library/inspector/cli-codegen-3.spec.ts | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 261a0eab21..4cfa512a4f 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -578,8 +578,8 @@ class TextAssertionTool implements RecorderTool { onPointerUp(event: PointerEvent) { const target = this._hoverHighlight?.elements[0]; - if (this._kind === 'value' && target && target.nodeName === 'INPUT' && (target as HTMLInputElement).disabled) { - // Click on a disabled input does not produce a "click" event, but we still want + if (this._kind === 'value' && target && (target.nodeName === 'INPUT' || target.nodeName === 'SELECT') && (target as HTMLInputElement).disabled) { + // Click on a disabled input (or select) does not produce a "click" event, but we still want // to assert the value. this._commitAssertValue(); } diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 96a6295f13..2c0fff974a 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -631,6 +631,27 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`); expect.soft(sources2.get('C#')!.text).toContain(`await Expect(page.Locator("#second")).ToHaveValueAsync("bar")`); }); + test('should assert value on disabled select', async ({ openRecorder, browserName }) => { + const recorder = await openRecorder(); + + await recorder.setContentAndWait(` + + + `); + + await recorder.page.click('x-pw-tool-item.value'); + await recorder.hoverOverElement('#second'); + const [sources2] = await Promise.all([ + recorder.waitForOutput('JavaScript', '#second'), + recorder.trustedClick(), + ]); + expect.soft(sources2.get('JavaScript')!.text).toContain(`await expect(page.locator('#second')).toHaveValue('bar2')`); + expect.soft(sources2.get('Python')!.text).toContain(`expect(page.locator("#second")).to_have_value("bar2")`); + expect.soft(sources2.get('Python Async')!.text).toContain(`await expect(page.locator("#second")).to_have_value("bar2")`); + expect.soft(sources2.get('Java')!.text).toContain(`assertThat(page.locator("#second")).hasValue("bar2")`); + expect.soft(sources2.get('C#')!.text).toContain(`await Expect(page.Locator("#second")).ToHaveValueAsync("bar2")`); + }); + test('should assert visibility', async ({ openRecorder }) => { const recorder = await openRecorder(); From 60773f34d839394f36efd82367f997b5af2be53a Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Sun, 30 Jun 2024 04:21:46 -0700 Subject: [PATCH 25/33] feat(webkit): roll to r2040 (#31486) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 761a95ce6b..c6278b1777 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2039", + "revision": "2040", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 7040340d61d65f573313f449aebd6481d720460a Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 1 Jul 2024 12:19:04 +0200 Subject: [PATCH 26/33] test: adjust upload folder expectation for msedge (#31454) --- tests/page/page-set-input-files.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index 22ce7151cd..76a53fa571 100644 --- a/tests/page/page-set-input-files.spec.ts +++ b/tests/page/page-set-input-files.spec.ts @@ -16,7 +16,7 @@ */ import { test as it, expect } from './pageTest'; -import { attachFrame, chromiumVersionLessThan } from '../config/utils'; +import { attachFrame } from '../config/utils'; import path from 'path'; import fs from 'fs'; @@ -37,7 +37,7 @@ it('should upload the file', async ({ page, server, asset }) => { }, input)).toBe('contents of the file'); }); -it('should upload a folder', async ({ page, server, browserName, headless, browserVersion, isAndroid }) => { +it('should upload a folder', async ({ page, server, browserName, headless, browserMajorVersion, isAndroid }) => { it.skip(isAndroid); it.skip(os.platform() === 'darwin' && parseInt(os.release().split('.')[0], 10) <= 21, 'WebKit on macOS-12 is frozen'); @@ -54,7 +54,7 @@ it('should upload a folder', async ({ page, server, browserName, headless, brows await input.setInputFiles(dir); expect(new Set(await page.evaluate(e => [...e.files].map(f => f.webkitRelativePath), input))).toEqual(new Set([ // https://issues.chromium.org/issues/345393164 - ...((browserName === 'chromium' && headless && !process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW && chromiumVersionLessThan(browserVersion, '127.0.6533.0')) ? [] : ['file-upload-test/sub-dir/really.txt']), + ...((browserName === 'chromium' && headless && !process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW && browserMajorVersion < 127) ? [] : ['file-upload-test/sub-dir/really.txt']), 'file-upload-test/file1.txt', 'file-upload-test/file2', ])); From 2136b96df1d99e121de24dd369a2e484be87b422 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Mon, 1 Jul 2024 07:14:32 -0700 Subject: [PATCH 27/33] feat(webkit): roll to r2041 (#31502) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index c6278b1777..8dff6bf676 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2040", + "revision": "2041", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From d1e76d9a9287af90e523b94eb5e5020be2ac88fc Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 1 Jul 2024 18:32:23 +0200 Subject: [PATCH 28/33] docs(release-notes): fix .NET snippets (#31496) --- docs/src/release-notes-csharp.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index 97b0c9cd50..9172478402 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -17,9 +17,9 @@ Utilizing the new [Clock] API allows to manipulate and control time within tests ```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 +await Page.Clock.InstallAsync(new() { - Time = new DateTime(2024, 2, 2, 8, 0, 0) + TimeDate = new DateTime(2024, 2, 2, 8, 0, 0) }); await Page.GotoAsync("http://localhost:3333"); @@ -28,11 +28,11 @@ await Page.GotoAsync("http://localhost:3333"); 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"); +await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("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"); +await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); ``` See [the clock guide](./clock.md) for more details. From b349a7364525169f7932837fd2cc0f732059f43e Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 1 Jul 2024 18:51:59 +0200 Subject: [PATCH 29/33] fix(ct): export package.json (#31504) --- packages/playwright-ct-react/package.json | 3 ++- packages/playwright-ct-react17/package.json | 3 ++- packages/playwright-ct-solid/package.json | 3 ++- packages/playwright-ct-svelte/package.json | 3 ++- packages/playwright-ct-vue/package.json | 3 ++- packages/playwright-ct-vue2/package.json | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/playwright-ct-react/package.json b/packages/playwright-ct-react/package.json index 01426b8b15..295af3f34a 100644 --- a/packages/playwright-ct-react/package.json +++ b/packages/playwright-ct-react/package.json @@ -26,7 +26,8 @@ "./hooks": { "types": "./hooks.d.ts", "default": "./hooks.mjs" - } + }, + "./package.json": "./package.json" }, "dependencies": { "@playwright/experimental-ct-core": "1.46.0-next", diff --git a/packages/playwright-ct-react17/package.json b/packages/playwright-ct-react17/package.json index ca94174e73..8d0fee1168 100644 --- a/packages/playwright-ct-react17/package.json +++ b/packages/playwright-ct-react17/package.json @@ -26,7 +26,8 @@ "./hooks": { "types": "./hooks.d.ts", "default": "./hooks.mjs" - } + }, + "./package.json": "./package.json" }, "dependencies": { "@playwright/experimental-ct-core": "1.46.0-next", diff --git a/packages/playwright-ct-solid/package.json b/packages/playwright-ct-solid/package.json index 07b34f44a4..ed46930149 100644 --- a/packages/playwright-ct-solid/package.json +++ b/packages/playwright-ct-solid/package.json @@ -26,7 +26,8 @@ "./hooks": { "types": "./hooks.d.ts", "default": "./hooks.mjs" - } + }, + "./package.json": "./package.json" }, "dependencies": { "@playwright/experimental-ct-core": "1.46.0-next", diff --git a/packages/playwright-ct-svelte/package.json b/packages/playwright-ct-svelte/package.json index 3a52fc70a5..2b35018c03 100644 --- a/packages/playwright-ct-svelte/package.json +++ b/packages/playwright-ct-svelte/package.json @@ -26,7 +26,8 @@ "./hooks": { "types": "./hooks.d.ts", "default": "./hooks.mjs" - } + }, + "./package.json": "./package.json" }, "dependencies": { "@playwright/experimental-ct-core": "1.46.0-next", diff --git a/packages/playwright-ct-vue/package.json b/packages/playwright-ct-vue/package.json index 66be15de8e..fdda54207a 100644 --- a/packages/playwright-ct-vue/package.json +++ b/packages/playwright-ct-vue/package.json @@ -26,7 +26,8 @@ "./hooks": { "types": "./hooks.d.ts", "default": "./hooks.mjs" - } + }, + "./package.json": "./package.json" }, "dependencies": { "@playwright/experimental-ct-core": "1.46.0-next", diff --git a/packages/playwright-ct-vue2/package.json b/packages/playwright-ct-vue2/package.json index 74d48c8b41..70981c4f9b 100644 --- a/packages/playwright-ct-vue2/package.json +++ b/packages/playwright-ct-vue2/package.json @@ -26,7 +26,8 @@ "./hooks": { "types": "./hooks.d.ts", "default": "./hooks.mjs" - } + }, + "./package.json": "./package.json" }, "dependencies": { "@playwright/experimental-ct-core": "1.46.0-next", From 9a3e0967e69dc10110e4c4363ca8f7ef20bbbf5c Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 1 Jul 2024 19:19:38 +0200 Subject: [PATCH 30/33] fix(electron): tracing with @playwright/test (#31437) --- packages/playwright/src/index.ts | 2 +- .../playwright-electron-should-work.spec.ts | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index a258cbcad5..79184cc135 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -609,7 +609,7 @@ class ArtifactsRecorder { if ((tracing as any)[this._startedCollectingArtifacts]) return; (tracing as any)[this._startedCollectingArtifacts] = true; - if (this._testInfo._tracing.traceOptions()) + if (this._testInfo._tracing.traceOptions() && (tracing as any)[kTracingStarted]) await tracing.stopChunk({ path: this._testInfo._tracing.generateNextTraceRecordingPath() }); } } diff --git a/tests/installation/playwright-electron-should-work.spec.ts b/tests/installation/playwright-electron-should-work.spec.ts index 4de28e8b54..5c6ced0948 100755 --- a/tests/installation/playwright-electron-should-work.spec.ts +++ b/tests/installation/playwright-electron-should-work.spec.ts @@ -15,6 +15,7 @@ */ import { test } from './npmTest'; import fs from 'fs'; +import { expect } from 'packages/playwright-test'; import path from 'path'; test('electron should work', async ({ exec, tsc, writeFiles }) => { @@ -39,3 +40,46 @@ test('electron should work with special characters in path', async ({ exec, tmpW cwd: path.join(folderName) }); }); + +test('should work when wrapped inside @playwright/test and trace is enabled', async ({ exec, tmpWorkspace, writeFiles }) => { + await exec('npm i -D @playwright/test electron@31'); + await writeFiles({ + 'electron-with-tracing.spec.ts': ` + import { test, expect, _electron } from '@playwright/test'; + + test('should work', async ({ trace }) => { + const electronApp = await _electron.launch({ args: [${JSON.stringify(path.join(__dirname, '../electron/electron-window-app.js'))}] }); + + const window = await electronApp.firstWindow(); + if (trace) + await window.context().tracing.start({ screenshots: true, snapshots: true }); + + await window.goto('data:text/html,Playwright

Playwright

'); + await expect(window).toHaveTitle(/Playwright/); + await expect(window.getByRole('heading')).toHaveText('Playwright'); + + 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' }); + } + await electronApp.close(); + }); + `, + }); + const jsonOutputName = test.info().outputPath('report.json'); + await exec('npx playwright test --trace=on --reporter=json electron-with-tracing.spec.ts', { + env: { PLAYWRIGHT_JSON_OUTPUT_NAME: jsonOutputName } + }); + const traces = [ + // our actual trace. + 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.zip'), + ]; + for (const trace of traces) + expect(fs.existsSync(trace)).toBe(true); + const report = JSON.parse(fs.readFileSync(jsonOutputName, 'utf-8')); + expect(new Set(['trace'])).toEqual(new Set(report.suites[0].specs[0].tests[0].results[0].attachments.map(a => a.name))); + expect(new Set(traces.map(p => fs.realpathSync(p)))).toEqual(new Set(report.suites[0].specs[0].tests[0].results[0].attachments.map(a => a.path))); +}); From 1f92376508a3ee3d28e4f68353b6a1a87b2ac372 Mon Sep 17 00:00:00 2001 From: KeisukeYamashita <19yamashita15@gmail.com> Date: Mon, 1 Jul 2024 19:44:17 +0200 Subject: [PATCH 31/33] docs(ci): added Drone CI docs for Node.js (#31499) --- docs/src/ci.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/src/ci.md b/docs/src/ci.md index 2556cbf241..b62f2ca980 100644 --- a/docs/src/ci.md +++ b/docs/src/ci.md @@ -601,6 +601,23 @@ steps: - 'CI=true' ``` +### Drone +* langs: js + +To run Playwright tests on Drone, use our public Docker image ([see Dockerfile](./docker.md)). + +```yml +kind: pipeline +name: default +type: docker + +steps: + - name: test + image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy + commands: + - npx playwright test +``` + ## Caching browsers Caching browser binaries is not recommended, since the amount of time it takes to restore the cache is comparable to the time it takes to download the binaries. Especially under Linux, [operating system dependencies](./browsers.md#install-system-dependencies) need to be installed, which are not cacheable. From f62121548a98ff16e3685e4174ba3cc4bfd2a6f6 Mon Sep 17 00:00:00 2001 From: Noah Mayerhofer Date: Mon, 1 Jul 2024 20:57:16 +0200 Subject: [PATCH 32/33] docs(ci): typo (#31508) --- docs/src/ci.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/ci.md b/docs/src/ci.md index b62f2ca980..69a59a70dc 100644 --- a/docs/src/ci.md +++ b/docs/src/ci.md @@ -650,7 +650,7 @@ By default, Playwright launches browsers in headless mode. See in our [Running t On Linux agents, headed execution requires [Xvfb](https://en.wikipedia.org/wiki/Xvfb) to be installed. Our [Docker image](./docker.md) and GitHub Action have Xvfb pre-installed. To run browsers in headed mode with Xvfb, add `xvfb-run` before the actual command. ```bash js -xvfb-run npx playwrght test +xvfb-run npx playwright test ``` ```bash python xvfb-run pytest From 9dc7e400849e26226a14084524e53900d7a08661 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 1 Jul 2024 22:00:03 +0200 Subject: [PATCH 33/33] chore(electron): don't swallow close errors (#31509) --- packages/playwright-core/src/client/electron.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index a9e90b77f2..fcabfa494f 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -29,7 +29,7 @@ import type { Page } from './page'; import { ConsoleMessage } from './consoleMessage'; import type { Env, WaitForEventOptions, Headers, BrowserContextOptions } from './types'; import { Waiter } from './waiter'; -import { TargetClosedError } from './errors'; +import { TargetClosedError, isTargetClosedError } from './errors'; type ElectronOptions = Omit & { env?: Env, @@ -116,7 +116,13 @@ export class ElectronApplication extends ChannelOwner {}); + try { + await this._context.close(); + } catch (e) { + if (isTargetClosedError(e)) + return; + throw e; + } } async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise {