);
}
diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx
index 8e478f95d7..a5f9a7a358 100644
--- a/packages/html-reporter/src/testFileView.tsx
+++ b/packages/html-reporter/src/testFileView.tsx
@@ -16,14 +16,13 @@
import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types';
import * as React from 'react';
-import { msToString } from './uiUtils';
+import { hashStringToInt, msToString } from './utils';
import { Chip } from './chip';
import { filterWithToken, type Filter } from './filter';
import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
import { statusIcon } from './statusIcon';
import './testFileView.css';
import { video, image, trace } from './icons';
-import { hashStringToInt } from './labelUtils';
export const TestFileView: React.FC
Date: Mon, 15 Jul 2024 22:34:57 +0100
Subject: [PATCH 059/376] fix: add 'window-management' to chromium browser
(#31687)
---
docs/src/api/class-browsercontext.md | 1 +
packages/playwright-core/src/server/chromium/crBrowser.ts | 1 +
packages/playwright-core/types/types.d.ts | 1 +
tests/library/permissions.spec.ts | 8 ++++++++
4 files changed, 11 insertions(+)
diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md
index 48542a2603..e5ccf7b5ce 100644
--- a/docs/src/api/class-browsercontext.md
+++ b/docs/src/api/class-browsercontext.md
@@ -980,6 +980,7 @@ A permission or an array of permissions to grant. Permissions can be one of the
* `'notifications'`
* `'payment-handler'`
* `'storage-access'`
+* `'window-management'`
### option: BrowserContext.grantPermissions.origin
* since: v1.8
diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts
index 777ff2eee8..9d1e672132 100644
--- a/packages/playwright-core/src/server/chromium/crBrowser.ts
+++ b/packages/playwright-core/src/server/chromium/crBrowser.ts
@@ -434,6 +434,7 @@ export class CRBrowserContext extends BrowserContext {
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
['storage-access', 'storageAccess'],
+ ['window-management', 'windowManagement']
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts
index f9e25c8c40..fc468defe0 100644
--- a/packages/playwright-core/types/types.d.ts
+++ b/packages/playwright-core/types/types.d.ts
@@ -8472,6 +8472,7 @@ export interface BrowserContext {
* - `'notifications'`
* - `'payment-handler'`
* - `'storage-access'`
+ * - `'window-management'`
* @param options
*/
grantPermissions(permissions: ReadonlyArray, options?: {
diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts
index ac2ec7a0b5..7e77bba10f 100644
--- a/tests/library/permissions.spec.ts
+++ b/tests/library/permissions.spec.ts
@@ -48,6 +48,14 @@ it.describe('permissions', () => {
expect(await getPermission(page, 'geolocation')).toBe('granted');
});
+ it('should grant window-management permission when origin is listed', async ({ page, context, server, browserName }) => {
+ it.fail(browserName === 'firefox');
+
+ await page.goto(server.EMPTY_PAGE);
+ await context.grantPermissions(['window-management'], { origin: server.EMPTY_PAGE });
+ expect(await getPermission(page, 'window-management')).toBe('granted');
+ });
+
it('should prompt for geolocation permission when origin is not listed', async ({ page, context, server }) => {
await page.goto(server.EMPTY_PAGE);
await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE });
From 37ffbd757e732b52c3497ff65009fbe3a5743df5 Mon Sep 17 00:00:00 2001
From: Yury Semikhatsky
Date: Mon, 15 Jul 2024 14:35:11 -0700
Subject: [PATCH 060/376] chore: remove unused project to id mapping from html
builder (#31698)
---
packages/playwright/src/reporters/html.ts | 18 ------------------
1 file changed, 18 deletions(-)
diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts
index 2a5bbc94d4..7b279a70a1 100644
--- a/packages/playwright/src/reporters/html.ts
+++ b/packages/playwright/src/reporters/html.ts
@@ -226,8 +226,6 @@ class HtmlBuilder {
private _dataZipFile: ZipFile;
private _hasTraces = false;
private _attachmentsBaseURL: string;
- private _projectToId: Map = new Map();
- private _lastProjectId = 0;
constructor(config: FullConfig, outputDir: string, attachmentsBaseURL: string) {
this._config = config;
@@ -406,16 +404,6 @@ class HtmlBuilder {
};
}
- private _projectId(suite: Suite): number {
- const project = projectSuite(suite);
- let id = this._projectToId.get(project);
- if (!id) {
- id = ++this._lastProjectId;
- this._projectToId.set(project, id);
- }
- return id;
- }
-
private _serializeAttachments(attachments: JsonAttachment[]) {
let lastAttachment: TestAttachment | undefined;
return attachments.map(a => {
@@ -653,10 +641,4 @@ function createSnippets(stepsInFile: MultiMap) {
}
}
-function projectSuite(suite: Suite): Suite {
- while (suite.parent?.parent)
- suite = suite.parent;
- return suite;
-}
-
export default HtmlReporter;
From a5ca9b7d37a9c09b6da62a5d18d929a9c44e054e Mon Sep 17 00:00:00 2001
From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com>
Date: Mon, 15 Jul 2024 23:28:58 -0700
Subject: [PATCH 061/376] feat(webkit): roll to r2047 (#31701)
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 7911ee8f1e..1f00780f4f 100644
--- a/packages/playwright-core/browsers.json
+++ b/packages/playwright-core/browsers.json
@@ -27,7 +27,7 @@
},
{
"name": "webkit",
- "revision": "2045",
+ "revision": "2047",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",
From 96e0a96ac1278ac58723c1f6b79d673be3fc2ee2 Mon Sep 17 00:00:00 2001
From: damar Zaky
Date: Tue, 16 Jul 2024 20:15:25 +0700
Subject: [PATCH 062/376] docs: fix grammar and typos in various files (#31678)
- docs/src/best-practices-js.md
- docs/src/codegen.md
- docs/src/debug.md
- docs/src/events.md
- docs/src/library-js.md
- docs/src/locators.md
- docs/src/other-locators.md
- docs/src/test-components-js.md
- docs/src/trace-viewer.md
---------
Signed-off-by: damar Zaky
---
docs/src/best-practices-js.md | 2 +-
docs/src/codegen.md | 8 ++++----
docs/src/debug.md | 12 ++++++------
docs/src/events.md | 2 +-
docs/src/library-js.md | 2 +-
docs/src/locators.md | 6 +++---
docs/src/other-locators.md | 2 +-
docs/src/test-components-js.md | 2 +-
docs/src/trace-viewer.md | 4 ++--
9 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/docs/src/best-practices-js.md b/docs/src/best-practices-js.md
index 02e43786e0..3694418533 100644
--- a/docs/src/best-practices-js.md
+++ b/docs/src/best-practices-js.md
@@ -214,7 +214,7 @@ Playwright comes with a range of tooling to help you write tests.
- The [VS Code extension](./getting-started-vscode.md) gives you a great developer experience when writing, running, and debugging tests.
- The [test generator](./codegen.md) can generate tests and pick locators for you.
- The [trace viewer](./trace-viewer.md) gives you a full trace of your tests as a local PWA that can easily be shared. With the trace viewer you can view the timeline, inspect DOM snapshots for each action, view network requests and more.
-- The [UI Mode](./test-ui-mode) let's you explore, run and debug tests with a time travel experience complete with watch mode. All test files are loaded into the testing sidebar where you can expand each file and describe block to individually run, view, watch and debug each test.
+- The [UI Mode](./test-ui-mode) lets you explore, run and debug tests with a time travel experience complete with watch mode. All test files are loaded into the testing sidebar where you can expand each file and describe block to individually run, view, watch and debug each test.
- [Typescript](./test-typescript) in Playwright works out of the box and gives you better IDE integrations. Your IDE will show you everything you can do and highlight when you do something wrong. No TypeScript experience is needed and it is not necessary for your code to be in TypeScript, all you need to do is create your tests with a `.ts` extension.
### Test across all browsers
diff --git a/docs/src/codegen.md b/docs/src/codegen.md
index b23c023ac2..4b45f12d01 100644
--- a/docs/src/codegen.md
+++ b/docs/src/codegen.md
@@ -125,14 +125,14 @@ With the test generator you can record:
When you have finished interacting with the page, press the **record** button to stop the recording and use the **copy** button to copy the generated code to your editor.
-Use the **clear** button to clear the code to start recording again. Once finished close the Playwright inspector window or stop the terminal command.
+Use the **clear** button to clear the code to start recording again. Once finished, close the Playwright inspector window or stop the terminal command.
### Generating locators
You can generate [locators](/locators.md) with the test generator.
* Press the `'Record'` button to stop the recording and the `'Pick Locator'` button will appear.
* Click on the `'Pick Locator'` button and then hover over elements in the browser window to see the locator highlighted underneath each element.
-* To choose a locator click on the element you would like to locate and the code for that locator will appear in the field next to the Pick Locator button.
+* To choose a locator, click on the element you would like to locate and the code for that locator will appear in the field next to the Pick Locator button.
* You can then edit the locator in this field to fine tune it or use the copy button to copy it and paste it into your code.
######
@@ -284,7 +284,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --color-scheme=dark playwright.dev
Record scripts and tests while emulating timezone, language & location using the `--timezone`, `--geolocation` and `--lang` options. Once the page opens:
1. Accept the cookies
-1. On the top right click on the locate me button to see geolocation in action.
+1. On the top right, click on the locate me button to see geolocation in action.
```bash js
npx playwright codegen --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --lang="it-IT" bing.com/maps
@@ -375,7 +375,7 @@ Make sure you only use the `auth.json` locally as it contains sensitive informat
#### Load authenticated state
-Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can can continue generating tests from the logged in state.
+Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can continue generating tests from the logged in state.
```bash js
npx playwright codegen --load-storage=auth.json github.com/microsoft/playwright
diff --git a/docs/src/debug.md b/docs/src/debug.md
index bc4b0c1a80..aff5077d3c 100644
--- a/docs/src/debug.md
+++ b/docs/src/debug.md
@@ -46,7 +46,7 @@ A browser window will open and the test will run and pause at where the breakpoi
### Debug in different Browsers
-By default debugging is done using the Chromium profile. You can debug your tests on different browsers by right clicking on the debug icon in the testing sidebar and clicking on the 'Select Default Profile' option from the dropdown.
+By default, debugging is done using the Chromium profile. You can debug your tests on different browsers by right clicking on the debug icon in the testing sidebar and clicking on the 'Select Default Profile' option from the dropdown.
@@ -80,7 +80,7 @@ npx playwright test --debug
```
#### Debug one test on all browsers
-To debug one test on a specific line run the test command followed by the name of the test file and the line number of the test you want to debug, followed by the `--debug` flag. This will run a single test in each browser configured in your [`playwright.config`](./test-projects.md#configure-projects-for-multiple-browsers) and open the inspector.
+To debug one test on a specific line, run the test command followed by the name of the test file and the line number of the test you want to debug, followed by the `--debug` flag. This will run a single test in each browser configured in your [`playwright.config`](./test-projects.md#configure-projects-for-multiple-browsers) and open the inspector.
```bash
npx playwright test example.spec.ts:10 --debug
@@ -207,7 +207,7 @@ While running in debug mode you can live edit the locators. Next to the 'Pick Lo
### Picking locators
-While debugging you might need to choose a more resilient locator. You can do this by clicking on the **Pick Locator** button and hovering over any element in the browser window. While hovering over an element you will see the code needed to locate this element highlighted below. Clicking an element in the browser will add the locator into the field where you can then either tweak it or copy it into your code.
+While debugging, you might need to choose a more resilient locator. You can do this by clicking on the **Pick Locator** button and hovering over any element in the browser window. While hovering over an element you will see the code needed to locate this element highlighted below. Clicking an element in the browser will add the locator into the field where you can then either tweak it or copy it into your code.
@@ -242,7 +242,7 @@ This will also set the default timeouts of Playwright to 0 (= no timeout).
-To debug your tests using the browser developer tools start by setting a breakpoint in your test to pause the execution using the [`method: Page.pause`] method.
+To debug your tests using the browser developer tools, start by setting a breakpoint in your test to pause the execution using the [`method: Page.pause`] method.
```js
await page.pause();
@@ -264,7 +264,7 @@ page.pause()
await page.PauseAsync();
```
-Once you have set a breakpoint in your test you can then run your test with `PWDEBUG=console`.
+Once you have set a breakpoint in your test, you can then run your test with `PWDEBUG=console`.
```bash tab=bash-bash lang=js
PWDEBUG=console npx playwright test
@@ -327,7 +327,7 @@ $env:PWDEBUG=console
dotnet test
```
-Once Playwright launches the browser window you can open the developer tools.
+Once Playwright launches the browser window, you can open the developer tools.
The `playwright` object will be available in the console panel.
#### playwright.$(selector)
diff --git a/docs/src/events.md b/docs/src/events.md
index 900cde1a95..23d3c90c3e 100644
--- a/docs/src/events.md
+++ b/docs/src/events.md
@@ -5,7 +5,7 @@ title: "Events"
## Introduction
-Playwright allows listening to various types of events happening on the web page, such as network requests, creation of child pages, dedicated workers etc. There are several ways to subscribe to such events such as waiting for events or adding or removing event listeners.
+Playwright allows listening to various types of events happening on the web page, such as network requests, creation of child pages, dedicated workers etc. There are several ways to subscribe to such events, such as waiting for events or adding or removing event listeners.
## Waiting for event
diff --git a/docs/src/library-js.md b/docs/src/library-js.md
index ae3d1916ec..9085290bba 100644
--- a/docs/src/library-js.md
+++ b/docs/src/library-js.md
@@ -103,7 +103,7 @@ The key differences to note are as follows:
| Installation | `npm install playwright` | `npm init playwright@latest` - note `install` vs. `init` |
| Install browsers | Install `@playwright/browser-chromium`, `@playwright/browser-firefox` and/or `@playwright/browser-webkit` | `npx playwright install` or `npx playwright install chromium` for a single one |
| `import` from | `playwright` | `@playwright/test` |
-| Initialization | Explicitly need to:
Pick a browser to use, e.g. `chromium`
Launch browser with [`method: BrowserType.launch`]
Create a context with [`method: Browser.newContext`], and pass any context options explicitly, e.g. `devices['iPhone 11']`
Create a page with [`method: BrowserContext.newPage`]
| An isolated `page` and `context` are provided to each test out-of the box, along with other [built-in fixtures](./test-fixtures.md#built-in-fixtures). No explicit creation. If referenced by the test in it's arguments, the Test Runner will create them for the test. (i.e. lazy-initialization) |
+| Initialization | Explicitly need to:
Pick a browser to use, e.g. `chromium`
Launch browser with [`method: BrowserType.launch`]
Create a context with [`method: Browser.newContext`], and pass any context options explicitly, e.g. `devices['iPhone 11']`
Create a page with [`method: BrowserContext.newPage`]
| An isolated `page` and `context` are provided to each test out-of the box, along with other [built-in fixtures](./test-fixtures.md#built-in-fixtures). No explicit creation. If referenced by the test in its arguments, the Test Runner will create them for the test. (i.e. lazy-initialization) |
| Assertions | No built-in Web-First Assertions | [Web-First assertions](./test-assertions.md) like:
[`method: PageAssertions.toHaveTitle`]
[`method: PageAssertions.toHaveScreenshot#1`]
which auto-wait and retry for the condition to be met.|
| Cleanup | Explicitly need to:
Close context with [`method: BrowserContext.close`]
Close browser with [`method: Browser.close`]
| No explicit close of [built-in fixtures](./test-fixtures.md#built-in-fixtures); the Test Runner will take care of it.
| Running | When using the Library, you run the code as a node script, possibly with some compilation first. | When using the Test Runner, you use the `npx playwright test` command. Along with your [config](./test-configuration.md), the Test Runner handles any compilation and choosing what to run and how to run it. |
diff --git a/docs/src/locators.md b/docs/src/locators.md
index 052894a172..8ade71efb7 100644
--- a/docs/src/locators.md
+++ b/docs/src/locators.md
@@ -10,7 +10,7 @@ a way to find element(s) on the page at any moment.
### Quick Guide
-These are the recommended built in locators.
+These are the recommended built-in locators.
- [`method: Page.getByRole`](#locate-by-role) to locate by explicit and implicit accessibility attributes.
- [`method: Page.getByText`](#locate-by-text) to locate by text content.
@@ -513,7 +513,7 @@ Use this locator when your element has the `title` attribute.
### Locate by test id
-Testing by test ids is the most resilient way of testing as even if your text or role of the attribute changes the test will still pass. QA's and developers should define explicit test ids and query them with [`method: Page.getByTestId`]. However testing by test ids is not user facing. If the role or text value is important to you then consider using user facing locators such as [role](#locate-by-role) and [text locators](#locate-by-text).
+Testing by test ids is the most resilient way of testing as even if your text or role of the attribute changes, the test will still pass. QA's and developers should define explicit test ids and query them with [`method: Page.getByTestId`]. However testing by test ids is not user facing. If the role or text value is important to you then consider using user facing locators such as [role](#locate-by-role) and [text locators](#locate-by-text).
For example, consider the following DOM structure.
@@ -1501,7 +1501,7 @@ For example, consider the following DOM structure:
```
-Locate an item by it's test id of "orange" and then click it.
+Locate an item by its test id of "orange" and then click it.
```js
await page.getByTestId('orange').click();
diff --git a/docs/src/other-locators.md b/docs/src/other-locators.md
index c37b97f6e6..b42407231d 100644
--- a/docs/src/other-locators.md
+++ b/docs/src/other-locators.md
@@ -668,7 +668,7 @@ We recommend [locating by label text](./locators.md#locate-by-label) instead of
Targeted input actions in Playwright automatically distinguish between labels and controls, so you can target the label to perform an action on the associated control.
-For example, consider the following DOM structure: ``. You can target the label by it's "Password" text using [`method: Page.getByText`]. However, the following actions will be performed on the input instead of the label:
+For example, consider the following DOM structure: ``. You can target the label by its "Password" text using [`method: Page.getByText`]. However, the following actions will be performed on the input instead of the label:
- [`method: Locator.click`] will click the label and automatically focus the input field;
- [`method: Locator.fill`] will fill the input field;
- [`method: Locator.inputValue`] will return the value of the input field;
diff --git a/docs/src/test-components-js.md b/docs/src/test-components-js.md
index 87cf263711..60d920ee73 100644
--- a/docs/src/test-components-js.md
+++ b/docs/src/test-components-js.md
@@ -458,7 +458,7 @@ test('slot', async ({ mount }) => {
### hooks
-You can use `beforeMount` and `afterMount` hooks to configure your app. This lets you setup things like your app router, fake server etc. giving you the flexibility you need. You can also pass custom configuration from the `mount` call from a test, which is accessible from the `hooksConfig` fixture. This includes any config that needs to be run before or after mounting the component. An example of configuring a router is provided below:
+You can use `beforeMount` and `afterMount` hooks to configure your app. This lets you set up things like your app router, fake server etc. giving you the flexibility you need. You can also pass custom configuration from the `mount` call from a test, which is accessible from the `hooksConfig` fixture. This includes any config that needs to be run before or after mounting the component. An example of configuring a router is provided below:
Date: Tue, 16 Jul 2024 15:55:35 +0200
Subject: [PATCH 063/376] fix(setInputFiles): throw when uploading file in
directory upload (#31706)
---
CONTRIBUTING.md | 2 +-
packages/playwright-core/src/server/dom.ts | 2 ++
tests/page/page-set-input-files.spec.ts | 12 ++++++++++--
3 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5b71ccde04..30e89e6bee 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -125,7 +125,7 @@ All API classes, methods, and events should have a description in [`docs/src`](h
To run the documentation linter, use:
```bash
-npm run doclint
+npm run doc
```
To build the documentation site locally and test how your changes will look in practice:
diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts
index 30eb04d3f6..52e648abe0 100644
--- a/packages/playwright-core/src/server/dom.ts
+++ b/packages/playwright-core/src/server/dom.ts
@@ -639,6 +639,8 @@ export class ElementHandle extends js.JSHandle {
throw injected.createStacklessError('Non-multiple file input can only accept single file');
if (directoryUpload && !inputElement.webkitdirectory)
throw injected.createStacklessError('File input does not support directories, pass individual files instead');
+ if (!directoryUpload && inputElement.webkitdirectory)
+ throw injected.createStacklessError('[webkitdirectory] input requires passing a path to a directory');
return inputElement;
}, { multiple, directoryUpload: !!localDirectory });
if (result === 'error:notconnected' || !result.asElement())
diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts
index 76a53fa571..a32db203bd 100644
--- a/tests/page/page-set-input-files.spec.ts
+++ b/tests/page/page-set-input-files.spec.ts
@@ -120,6 +120,15 @@ it('should throw when uploading a folder in a normal file upload input', async (
await expect(input.setInputFiles(dir)).rejects.toThrow('File input does not support directories, pass individual files instead');
});
+it('should throw when uploading a file in a directory upload input', async ({ page, server, isAndroid, asset }) => {
+ it.skip(isAndroid);
+ it.skip(os.platform() === 'darwin' && parseInt(os.release().split('.')[0], 10) <= 21, 'WebKit on macOS-12 is frozen');
+
+ await page.goto(server.PREFIX + '/input/folderupload.html');
+ const input = await page.$('input');
+ await expect(input.setInputFiles(asset('file to upload.txt'))).rejects.toThrow('[webkitdirectory] input requires passing a path to a directory');
+});
+
it('should upload a file after popup', async ({ page, server, asset }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29923' });
await page.goto(server.PREFIX + '/input/fileupload.html');
@@ -341,8 +350,7 @@ it('should emit event via prepend', async ({ page, server }) => {
expect(chooser).toBeTruthy();
});
-it('should emit event for iframe', async ({ page, server, browserName }) => {
- it.skip(browserName === 'firefox');
+it('should emit event for iframe', async ({ page, server }) => {
const frame = await attachFrame(page, 'frame1', server.EMPTY_PAGE);
await frame.setContent(``);
const [chooser] = await Promise.all([
From f66f5b800ea925662139e202470b35e4e3d29f3b Mon Sep 17 00:00:00 2001
From: Max Schmitt
Date: Tue, 16 Jul 2024 16:11:20 +0200
Subject: [PATCH 064/376] docs: fix broken anchor links (#31707)
---
docs/src/other-locators.md | 10 +---------
docs/src/release-notes-java.md | 2 +-
docs/src/release-notes-js.md | 2 +-
docs/src/release-notes-python.md | 2 +-
4 files changed, 4 insertions(+), 12 deletions(-)
diff --git a/docs/src/other-locators.md b/docs/src/other-locators.md
index b42407231d..c7f6e234d1 100644
--- a/docs/src/other-locators.md
+++ b/docs/src/other-locators.md
@@ -881,7 +881,7 @@ await page.Locator("data-test-id=submit").ClickAsync();
```
:::note
-Attribute selectors are not CSS selectors, so anything CSS-specific like `:enabled` is not supported. For more features, use a proper [css] selector, e.g. `css=[data-test="login"]:enabled`.
+Attribute selectors are not CSS selectors, so anything CSS-specific like `:enabled` is not supported. For more features, use a proper [css](#css-locator) selector, e.g. `css=[data-test="login"]:enabled`.
:::
## Chaining selectors
@@ -918,11 +918,3 @@ We recommend [filtering by another locator](./locators.md#filter-by-childdescend
By default, chained selectors resolve to an element queried by the last selector. A selector can be prefixed with `*` to capture elements that are queried by an intermediate selector.
For example, `css=article >> text=Hello` captures the element with the text `Hello`, and `*css=article >> text=Hello` (note the `*`) captures the `article` element that contains some element with the text `Hello`.
-
-
-[text]: #text-selector
-[css]: #css-selector
-[xpath]: #xpath-selectors
-[react]: #react-selectors
-[vue]: #vue-selectors
-[id]: #id-data-testid-data-test-id-data-test-selectors
diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md
index 6dce675b8f..54c4963216 100644
--- a/docs/src/release-notes-java.md
+++ b/docs/src/release-notes-java.md
@@ -1457,7 +1457,7 @@ This version of Playwright was also tested against the following stable channels
#### New APIs
-- [`browserType.launch()`](./api/class-browsertype#browsertypelaunchoptions) now accepts the new `'channel'` option. Read more in [our documentation](./browsers).
+- [`method: BrowserType.launch`] now accepts the new `'channel'` option. Read more in [our documentation](./browsers).
## Version 1.9
diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md
index 18f07daa14..cd4ec8354e 100644
--- a/docs/src/release-notes-js.md
+++ b/docs/src/release-notes-js.md
@@ -2651,7 +2651,7 @@ This version of Playwright was also tested against the following stable channels
#### New APIs
-- [`browserType.launch()`](./api/class-browsertype#browsertypelaunchoptions) now accepts the new `'channel'` option. Read more in [our documentation](./browsers).
+- [`method: BrowserType.launch`] now accepts the new `'channel'` option. Read more in [our documentation](./browsers).
## Version 1.9
diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md
index e7a4341a7e..25fbd3ef04 100644
--- a/docs/src/release-notes-python.md
+++ b/docs/src/release-notes-python.md
@@ -1433,7 +1433,7 @@ This version of Playwright was also tested against the following stable channels
#### New APIs
-- [`browserType.launch()`](./api/class-browsertype#browsertypelaunchoptions) now accepts the new `'channel'` option. Read more in [our documentation](./browsers).
+- [`method: BrowserType.launch`] now accepts the new `'channel'` option. Read more in [our documentation](./browsers).
## Version 1.9
From 8021312c99e242138d7eb0aec1036cd320a93996 Mon Sep 17 00:00:00 2001
From: Yury Semikhatsky
Date: Tue, 16 Jul 2024 09:44:38 -0700
Subject: [PATCH 065/376] chore: enable notification permission tests in WebKit
(#31699)
The Notifications API has been supported in WebKit since 2022, enable
related permission and tests.
---
packages/playwright-core/src/server/webkit/wkPage.ts | 1 +
tests/library/permissions.spec.ts | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts
index adfd06c05e..ae691f0ea3 100644
--- a/packages/playwright-core/src/server/webkit/wkPage.ts
+++ b/packages/playwright-core/src/server/webkit/wkPage.ts
@@ -1176,6 +1176,7 @@ export class WKPage implements PageDelegate {
async _grantPermissions(origin: string, permissions: string[]) {
const webPermissionToProtocol = new Map([
['geolocation', 'geolocation'],
+ ['notifications', 'notifications'],
['clipboard-read', 'clipboard-read'],
]);
const filtered = permissions.map(permission => {
diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts
index 7e77bba10f..ad56b418d3 100644
--- a/tests/library/permissions.spec.ts
+++ b/tests/library/permissions.spec.ts
@@ -22,7 +22,7 @@ function getPermission(page, name) {
}
it.describe('permissions', () => {
- it.skip(({ browserName }) => browserName === 'webkit', 'Permissions API is not implemented in WebKit (see https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API)');
+ it.fixme(({ browserName, isWindows }) => browserName === 'webkit' && isWindows, 'Permissions API is disabled on Windows WebKit');
it('should be prompt by default', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
@@ -49,7 +49,7 @@ it.describe('permissions', () => {
});
it('should grant window-management permission when origin is listed', async ({ page, context, server, browserName }) => {
- it.fail(browserName === 'firefox');
+ it.skip(browserName !== 'chromium', 'Only Chromium supports window management API.');
await page.goto(server.EMPTY_PAGE);
await context.grantPermissions(['window-management'], { origin: server.EMPTY_PAGE });
From 6a9e60d6a17b2a5b88c2bfa6826a9cec5b37fe0a Mon Sep 17 00:00:00 2001
From: Max Schmitt
Date: Tue, 16 Jul 2024 19:32:51 +0200
Subject: [PATCH 066/376] fix(ct): import ct* flavour types from ct-core and
then from pwt (#31642)
---
packages/playwright-ct-core/index.d.ts | 2 +-
packages/playwright-ct-react/index.d.ts | 6 ++----
packages/playwright-ct-react17/index.d.ts | 6 ++----
packages/playwright-ct-solid/index.d.ts | 6 ++----
packages/playwright-ct-svelte/index.d.ts | 6 ++----
packages/playwright-ct-vue/index.d.ts | 6 ++----
packages/playwright-ct-vue2/index.d.ts | 6 ++----
7 files changed, 13 insertions(+), 25 deletions(-)
diff --git a/packages/playwright-ct-core/index.d.ts b/packages/playwright-ct-core/index.d.ts
index b397169b74..1006bd59ec 100644
--- a/packages/playwright-ct-core/index.d.ts
+++ b/packages/playwright-ct-core/index.d.ts
@@ -56,4 +56,4 @@ export function defineConfig(config: PlaywrightTestConfig, ...configs: Playwrigh
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
-export { expect, devices } from 'playwright/test';
+export { expect, devices, Locator } from 'playwright/test';
diff --git a/packages/playwright-ct-react/index.d.ts b/packages/playwright-ct-react/index.d.ts
index c086d4bec5..c5e6d6d2da 100644
--- a/packages/playwright-ct-react/index.d.ts
+++ b/packages/playwright-ct-react/index.d.ts
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import type { Locator } from 'playwright/test';
-import type { TestType } from '@playwright/experimental-ct-core';
+import type { TestType, Locator } from '@playwright/experimental-ct-core';
export interface MountOptions {
hooksConfig?: HooksConfig;
@@ -33,5 +32,4 @@ export const test: TestType<{
): Promise;
}>;
-export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core';
-export { expect, devices } from 'playwright/test';
+export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core';
diff --git a/packages/playwright-ct-react17/index.d.ts b/packages/playwright-ct-react17/index.d.ts
index c086d4bec5..748212c45e 100644
--- a/packages/playwright-ct-react17/index.d.ts
+++ b/packages/playwright-ct-react17/index.d.ts
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import type { Locator } from 'playwright/test';
-import type { TestType } from '@playwright/experimental-ct-core';
+import type { TestType, Locator} from '@playwright/experimental-ct-core';
export interface MountOptions {
hooksConfig?: HooksConfig;
@@ -33,5 +32,4 @@ export const test: TestType<{
): Promise;
}>;
-export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core';
-export { expect, devices } from 'playwright/test';
+export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core';
diff --git a/packages/playwright-ct-solid/index.d.ts b/packages/playwright-ct-solid/index.d.ts
index c086d4bec5..c5e6d6d2da 100644
--- a/packages/playwright-ct-solid/index.d.ts
+++ b/packages/playwright-ct-solid/index.d.ts
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import type { Locator } from 'playwright/test';
-import type { TestType } from '@playwright/experimental-ct-core';
+import type { TestType, Locator } from '@playwright/experimental-ct-core';
export interface MountOptions {
hooksConfig?: HooksConfig;
@@ -33,5 +32,4 @@ export const test: TestType<{
): Promise;
}>;
-export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core';
-export { expect, devices } from 'playwright/test';
+export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core';
diff --git a/packages/playwright-ct-svelte/index.d.ts b/packages/playwright-ct-svelte/index.d.ts
index eb6d464032..761831f331 100644
--- a/packages/playwright-ct-svelte/index.d.ts
+++ b/packages/playwright-ct-svelte/index.d.ts
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import type { Locator } from 'playwright/test';
import type { SvelteComponent, ComponentProps } from 'svelte/types/runtime';
-import type { TestType } from '@playwright/experimental-ct-core';
+import type { TestType, Locator } from '@playwright/experimental-ct-core';
type ComponentSlot = string | string[];
type ComponentSlots = Record & { default?: ComponentSlot };
@@ -44,5 +43,4 @@ export const test: TestType<{
): Promise>;
}>;
-export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core';
-export { expect, devices } from 'playwright/test';
+export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core';
diff --git a/packages/playwright-ct-vue/index.d.ts b/packages/playwright-ct-vue/index.d.ts
index fee68f3cd0..2d182d4588 100644
--- a/packages/playwright-ct-vue/index.d.ts
+++ b/packages/playwright-ct-vue/index.d.ts
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import type { Locator } from 'playwright/test';
-import type { TestType } from '@playwright/experimental-ct-core';
+import type { TestType, Locator } from '@playwright/experimental-ct-core';
type ComponentSlot = string | string[];
type ComponentSlots = Record & { default?: ComponentSlot };
@@ -64,5 +63,4 @@ export const test: TestType<{
): Promise>;
}>;
-export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core';
-export { expect, devices } from 'playwright/test';
+export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core';
diff --git a/packages/playwright-ct-vue2/index.d.ts b/packages/playwright-ct-vue2/index.d.ts
index f1a2a0489f..133b4a60f2 100644
--- a/packages/playwright-ct-vue2/index.d.ts
+++ b/packages/playwright-ct-vue2/index.d.ts
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import type { Locator } from 'playwright/test';
-import type { TestType } from '@playwright/experimental-ct-core';
+import type { TestType, Locator } from '@playwright/experimental-ct-core';
type Slot = string | string[];
type ComponentSlots = Record & { default?: Slot };
@@ -64,5 +63,4 @@ export const test: TestType<{
): Promise>;
}>;
-export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core';
-export { expect, devices } from 'playwright/test';
+export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core';
From bb2e9d1175198290556b387d8987609d2e85565a Mon Sep 17 00:00:00 2001
From: Max Schmitt
Date: Tue, 16 Jul 2024 20:55:12 +0200
Subject: [PATCH 067/376] chore: make 'npm run clean' ignore .DS_Store (#31710)
---
utils/build/clean.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/utils/build/clean.js b/utils/build/clean.js
index b0ad6f2278..53079baf29 100644
--- a/utils/build/clean.js
+++ b/utils/build/clean.js
@@ -11,7 +11,9 @@ for (const pkg of workspace.packages()) {
rmSync(path.join(pkg.path, 'src', 'generated'));
const bundles = path.join(pkg.path, 'bundles');
if (fs.existsSync(bundles) && fs.statSync(bundles).isDirectory()) {
- for (const bundle of fs.readdirSync(bundles))
- rmSync(path.join(bundles, bundle, 'node_modules'));
+ for (const bundle of fs.readdirSync(bundles, { withFileTypes: true })) {
+ if (bundle.isDirectory())
+ rmSync(path.join(bundles, bundle.name, 'node_modules'));
+ }
}
}
From 3694c1422d9a541776602fb870d0b8e249eff35d Mon Sep 17 00:00:00 2001
From: Max Schmitt
Date: Tue, 16 Jul 2024 21:16:55 +0200
Subject: [PATCH 068/376] Revert "test: rebase golden snapshots on Chromium
macOS arm64 (#31344)" (#31711)
This reverts commit 02416877da13d21b277bc81221602e210171f6c7.
Since we landed
https://github.com/microsoft/playwright/commit/3127571b24e1a8a93960fa4a24fa011c3df20989
- we should revert this one as well.
---
tests/page/page-screenshot.spec.ts | 5 ++---
.../screenshot-canvas-macOS-arm64-chromium.png | Bin 1939 -> 0 bytes
2 files changed, 2 insertions(+), 3 deletions(-)
delete mode 100644 tests/page/page-screenshot.spec.ts-snapshots/screenshot-canvas-macOS-arm64-chromium.png
diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts
index dd1be3fbaa..7f374ac59b 100644
--- a/tests/page/page-screenshot.spec.ts
+++ b/tests/page/page-screenshot.spec.ts
@@ -280,13 +280,12 @@ it.describe('page screenshot', () => {
expect(screenshot).toMatchSnapshot('screenshot-clip-odd-size.png');
});
- it('should work for canvas', async ({ page, server, isElectron, isMac, browserName }) => {
+ it('should work for canvas', async ({ page, server, isElectron, isMac }) => {
it.fixme(isElectron && isMac, 'Fails on the bots');
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/screenshots/canvas.html');
const screenshot = await page.screenshot();
- const screenshotPrefix = browserName === 'chromium' && isMac && process.arch === 'arm64' ? '-macOS-arm64' : '';
- expect(screenshot).toMatchSnapshot(`screenshot-canvas${screenshotPrefix}.png`);
+ expect(screenshot).toMatchSnapshot('screenshot-canvas.png');
});
it('should capture canvas changes', async ({ page, isElectron, browserName, isMac, isWebView2 }) => {
diff --git a/tests/page/page-screenshot.spec.ts-snapshots/screenshot-canvas-macOS-arm64-chromium.png b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-canvas-macOS-arm64-chromium.png
deleted file mode 100644
index 830872e8d2e6ac8f68cc70a8d99bb838b2270fbc..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1939
zcmeAS@N?(olHy`uVBq!ia0y~yVEh8Y985qFmn|N5ffQqLkh>GZx^prw85r1oJzX3_
zD(1YsYnXS~K!D+(!H2_Nis>UG%Ppb=u~gaDTl0
zclPh@#};Az9~RF|XU;qEj8EW;-|XL)^Q=GReP+0EhjG>sSp|&OxTRU8sQT*?>Wb&V%H@S~l-@SWJ{KkZ{a4%$h-=7-yvyQhQ+KjUB1V+aKLtx}E
zGpHklCS4K--BRJGMfA(@qhUQ7)^x03sG8g^h+imH$hkdliaxNGW$<+Mb6Mw<&;$TH
CDVNRw
From e11c0c0cbb177fbf4fd208f181b99ba5f6109f9e Mon Sep 17 00:00:00 2001
From: Max Schmitt
Date: Wed, 17 Jul 2024 09:00:47 +0200
Subject: [PATCH 069/376] fix(connect): annotate internal api calls correctly
(#31715)
---
.../playwright-core/src/client/browserType.ts | 4 +-
.../playwright-core/src/client/connection.ts | 2 +
.../playwright.reuse.browser.spec.ts | 51 +++++++++++++++++++
3 files changed, 55 insertions(+), 2 deletions(-)
diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts
index 97d82de92d..9df049a641 100644
--- a/packages/playwright-core/src/client/browserType.ts
+++ b/packages/playwright-core/src/client/browserType.ts
@@ -155,7 +155,7 @@ export class BrowserType extends ChannelOwner imple
connection.close(reason || closeError);
};
pipe.on('closed', params => onPipeClosed(params.reason));
- connection.onmessage = message => pipe.send({ message }).catch(() => onPipeClosed());
+ connection.onmessage = message => this._wrapApiCall(() => pipe.send({ message }).catch(() => onPipeClosed()), /* isInternal */ true);
pipe.on('message', ({ message }) => {
try {
@@ -181,7 +181,7 @@ export class BrowserType extends ChannelOwner imple
this._didLaunchBrowser(browser, {}, logger);
browser._shouldCloseConnectionOnClose = true;
browser._connectHeaders = connectHeaders;
- browser.on(Events.Browser.Disconnected, closePipe);
+ browser.on(Events.Browser.Disconnected, () => this._wrapApiCall(() => closePipe(), /* isInternal */ true));
return browser;
}, deadline);
if (!result.timedOut) {
diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts
index 294b5d9f6e..953fc279e9 100644
--- a/packages/playwright-core/src/client/connection.ts
+++ b/packages/playwright-core/src/client/connection.ts
@@ -194,6 +194,8 @@ export class Connection extends EventEmitter {
}
close(cause?: string) {
+ if (this._closedError)
+ return;
this._closedError = new TargetClosedError(cause);
for (const callback of this._callbacks.values())
callback.reject(this._closedError);
diff --git a/tests/playwright-test/playwright.reuse.browser.spec.ts b/tests/playwright-test/playwright.reuse.browser.spec.ts
index ecada652a4..bccce95b3e 100644
--- a/tests/playwright-test/playwright.reuse.browser.spec.ts
+++ b/tests/playwright-test/playwright.reuse.browser.spec.ts
@@ -98,3 +98,54 @@ test('should reuse browser with special characters in the launch options', async
expect(guid2).toBe(guid1);
expect(workerIndex2).not.toBe(workerIndex1);
});
+
+test('should produce correct test steps', async ({ runInlineTest, runServer }) => {
+ const server = await runServer();
+ const result = await runInlineTest({
+ 'reporter.ts': `
+ class Reporter {
+ onStepBegin(test, result, step) {
+ console.log('%% onStepBegin ' + step.title);
+ }
+ onStepEnd(test, result, step) {
+ console.log('%% onStepEnd ' + step.title);
+ }
+ }
+ module.exports = Reporter;
+ `,
+ 'src/a.test.ts': `
+ import { test, expect } from '@playwright/test';
+ test('a', async ({ page }) => {
+ await page.goto('about:blank');
+ await page.evaluate(() => console.log('hello'));
+ });
+ `,
+ }, { reporter: './reporter.ts,list' }, { PW_TEST_REUSE_CONTEXT: '1', PW_TEST_CONNECT_WS_ENDPOINT: server.wsEndpoint() });
+
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+ expect(result.outputLines).toEqual([
+ 'onStepBegin Before Hooks',
+ 'onStepBegin fixture: browser',
+ 'onStepBegin browserType.connect',
+ 'onStepEnd browserType.connect',
+ 'onStepEnd fixture: browser',
+ 'onStepBegin fixture: context',
+ 'onStepEnd fixture: context',
+ 'onStepBegin fixture: page',
+ 'onStepBegin browserContext.newPage',
+ 'onStepEnd browserContext.newPage',
+ 'onStepEnd fixture: page',
+ 'onStepEnd Before Hooks',
+ 'onStepBegin page.goto(about:blank)',
+ 'onStepEnd page.goto(about:blank)',
+ 'onStepBegin page.evaluate',
+ 'onStepEnd page.evaluate',
+ 'onStepBegin After Hooks',
+ 'onStepBegin fixture: page',
+ 'onStepEnd fixture: page',
+ 'onStepBegin fixture: context',
+ 'onStepEnd fixture: context',
+ 'onStepEnd After Hooks'
+ ]);
+});
\ No newline at end of file
From ed6abf86c743c50baf3b0f033c8c5ef1f707172b Mon Sep 17 00:00:00 2001
From: Max Schmitt
Date: Wed, 17 Jul 2024 13:22:00 +0200
Subject: [PATCH 070/376] fix(expect): throw unsupported error when using
this.equals() in expect (#31723)
---
docs/src/test-assertions-js.md | 2 ++
packages/playwright/src/matchers/expect.ts | 5 +++++
tests/playwright-test/expect.spec.ts | 24 ++++++++++++++++++++++
3 files changed, 31 insertions(+)
diff --git a/docs/src/test-assertions-js.md b/docs/src/test-assertions-js.md
index 4745659cf9..defbc3a293 100644
--- a/docs/src/test-assertions-js.md
+++ b/docs/src/test-assertions-js.md
@@ -308,6 +308,8 @@ test('amount', async () => {
});
```
+### Compatibility with expect library
+
:::note
Do not confuse Playwright's `expect` with the [`expect` library](https://jestjs.io/docs/expect). The latter is not fully integrated with Playwright test runner, so make sure to use Playwright's own `expect`.
:::
diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts
index 9a010992da..3e49360eb7 100644
--- a/packages/playwright/src/matchers/expect.ts
+++ b/packages/playwright/src/matchers/expect.ts
@@ -138,6 +138,7 @@ function createExpect(info: ExpectMetaInfo) {
utils,
timeout: currentExpectTimeout()
};
+ (newThis as any).equals = throwUnsupportedExpectMatcherError;
return (matcher as any).call(newThis, ...args);
};
}
@@ -183,6 +184,10 @@ function createExpect(info: ExpectMetaInfo) {
return expectInstance;
}
+function throwUnsupportedExpectMatcherError() {
+ throw new Error('It looks like you are using custom expect matchers that are not compatible with Playwright. See https://aka.ms/playwright/expect-compatibility');
+}
+
expectLibrary.setState({ expand: false });
const customAsyncMatchers = {
diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts
index 1393bdd3ff..63928e86fb 100644
--- a/tests/playwright-test/expect.spec.ts
+++ b/tests/playwright-test/expect.spec.ts
@@ -1039,3 +1039,27 @@ test('should expose timeout to custom matchers', async ({ runInlineTest, runTSC
expect(result.failed).toBe(0);
expect(result.passed).toBe(2);
});
+
+test('should throw error when using .equals()', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'helper.ts': `
+ import { test as base, expect } from '@playwright/test';
+ expect.extend({
+ toBeWithinRange(received, floor, ceiling) {
+ this.equals(1, 2);
+ },
+ });
+ export const test = base;
+ `,
+ 'expect-test.spec.ts': `
+ import { test } from './helper';
+ test('numeric ranges', () => {
+ test.expect(() => {
+ test.expect(100).toBeWithinRange(90, 110);
+ }).toThrowError('It looks like you are using custom expect matchers that are not compatible with Playwright. See https://aka.ms/playwright/expect-compatibility');
+ });
+ `
+ });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+});
From 8eab28d858fdf7df9077aff24e4047cd34ae0c39 Mon Sep 17 00:00:00 2001
From: Simon Knott
Date: Wed, 17 Jul 2024 13:36:37 +0200
Subject: [PATCH 071/376] fix(list reporter): print step ends in non-TTY mode
(#31703)
When used in a terminal, the `list` reporter prints out information
about test steps to help debugging. In non-TTY environments like GitHub
Actions, currently it doesn't.
This PR changes that, so that in non-TTY environments you'll see the
"step end" messages appearing, but not the "step begin" messages. This
is a good middleground, because it helps the user understand test
progress, without being too verbose.
Closes https://github.com/microsoft/playwright/issues/31674
---
packages/playwright/src/reporters/list.ts | 46 +++++-----
tests/playwright-test/reporter-blob.spec.ts | 50 ++++++-----
tests/playwright-test/reporter-list.spec.ts | 95 ++++++++++++++-------
3 files changed, 118 insertions(+), 73 deletions(-)
diff --git a/packages/playwright/src/reporters/list.ts b/packages/playwright/src/reporters/list.ts
index 94e507fd4b..f87a7d4fe8 100644
--- a/packages/playwright/src/reporters/list.ts
+++ b/packages/playwright/src/reporters/list.ts
@@ -36,7 +36,7 @@ class ListReporter extends BaseReporter {
constructor(options: { printSteps?: boolean } = {}) {
super();
- this._printSteps = isTTY && getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps);
+ this._printSteps = getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps);
}
override printsToStdio() {
@@ -54,11 +54,13 @@ class ListReporter extends BaseReporter {
override onTestBegin(test: TestCase, result: TestResult) {
super.onTestBegin(test, result);
+
+ const index = String(this._resultIndex.size + 1);
+ this._resultIndex.set(result, index);
+
if (!isTTY)
return;
this._maybeWriteNewLine();
- const index = String(this._resultIndex.size + 1);
- this._resultIndex.set(result, index);
this._testRows.set(test, this._lastRow);
const prefix = this._testPrefix(index, '');
const line = colors.dim(formatTestTitle(this.config, test)) + this._retrySuffix(result);
@@ -75,28 +77,34 @@ class ListReporter extends BaseReporter {
this._dumpToStdio(test, chunk, process.stderr);
}
- override onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
- super.onStepBegin(test, result, step);
- if (step.category !== 'test.step')
- return;
- const testIndex = this._resultIndex.get(result) || '';
- if (!this._printSteps) {
- if (isTTY)
- this._updateLine(this._testRows.get(test)!, colors.dim(formatTestTitle(this.config, test, step)) + this._retrySuffix(result), this._testPrefix(testIndex, ''));
- return;
- }
+ private getStepIndex(testIndex: string, result: TestResult, step: TestStep): string {
+ if (this._stepIndex.has(step))
+ return this._stepIndex.get(step)!;
const ordinal = ((result as any)[lastStepOrdinalSymbol] || 0) + 1;
(result as any)[lastStepOrdinalSymbol] = ordinal;
const stepIndex = `${testIndex}.${ordinal}`;
this._stepIndex.set(step, stepIndex);
+ return stepIndex;
+ }
- if (isTTY) {
+ override onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
+ super.onStepBegin(test, result, step);
+ if (step.category !== 'test.step')
+ return;
+ const testIndex = this._resultIndex.get(result) || '';
+
+ if (!isTTY)
+ return;
+
+ if (this._printSteps) {
this._maybeWriteNewLine();
this._stepRows.set(step, this._lastRow);
- const prefix = this._testPrefix(stepIndex, '');
+ const prefix = this._testPrefix(this.getStepIndex(testIndex, result, step), '');
const line = test.title + colors.dim(stepSuffix(step));
this._appendLine(line, prefix);
+ } else {
+ this._updateLine(this._testRows.get(test)!, colors.dim(formatTestTitle(this.config, test, step)) + this._retrySuffix(result), this._testPrefix(testIndex, ''));
}
}
@@ -112,8 +120,8 @@ class ListReporter extends BaseReporter {
return;
}
- const index = this._stepIndex.get(step)!;
- const title = test.title + colors.dim(stepSuffix(step));
+ const index = this.getStepIndex(testIndex, result, step);
+ const title = isTTY ? test.title + colors.dim(stepSuffix(step)) : formatTestTitle(this.config, test, step);
const prefix = this._testPrefix(index, '');
let text = '';
if (step.error)
@@ -204,7 +212,7 @@ class ListReporter extends BaseReporter {
private _appendLine(text: string, prefix: string) {
const line = prefix + this.fitToScreen(text, prefix);
if (process.env.PW_TEST_DEBUG_REPORTERS) {
- process.stdout.write(this._lastRow + ' : ' + line + '\n');
+ process.stdout.write('#' + this._lastRow + ' : ' + line + '\n');
} else {
process.stdout.write(line);
process.stdout.write('\n');
@@ -215,7 +223,7 @@ class ListReporter extends BaseReporter {
private _updateLine(row: number, text: string, prefix: string) {
const line = prefix + this.fitToScreen(text, prefix);
if (process.env.PW_TEST_DEBUG_REPORTERS)
- process.stdout.write(row + ' : ' + line + '\n');
+ process.stdout.write('#' + row + ' : ' + line + '\n');
else
this._updateLineForTTY(row, line);
}
diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts
index 99882777a8..8f3064a073 100644
--- a/tests/playwright-test/reporter-blob.spec.ts
+++ b/tests/playwright-test/reporter-blob.spec.ts
@@ -478,28 +478,36 @@ test('merge into list report by default', async ({ runInlineTest, mergeReports }
const text = stripAnsi(output);
expect(text).toContain('Running 10 tests using 3 workers');
- const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
+ const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
expect(lines).toEqual([
- `0 : 1 a.test.js:3:11 › math 1`,
- `0 : ${POSITIVE_STATUS_MARK} 1 a.test.js:3:11 › math 1 (Xms)`,
- `1 : 2 a.test.js:6:11 › failing 1`,
- `1 : ${NEGATIVE_STATUS_MARK} 2 a.test.js:6:11 › failing 1 (Xms)`,
- `2 : 3 a.test.js:6:11 › failing 1 (retry #1)`,
- `2 : ${NEGATIVE_STATUS_MARK} 3 a.test.js:6:11 › failing 1 (retry #1) (Xms)`,
- `3 : 4 a.test.js:9:11 › flaky 1`,
- `3 : ${NEGATIVE_STATUS_MARK} 4 a.test.js:9:11 › flaky 1 (Xms)`,
- `4 : 5 a.test.js:9:11 › flaky 1 (retry #1)`,
- `4 : ${POSITIVE_STATUS_MARK} 5 a.test.js:9:11 › flaky 1 (retry #1) (Xms)`,
- `5 : 6 a.test.js:12:12 › skipped 1`,
- `5 : - 6 a.test.js:12:12 › skipped 1`,
- `6 : 7 b.test.js:3:11 › math 2`,
- `6 : ${POSITIVE_STATUS_MARK} 7 b.test.js:3:11 › math 2 (Xms)`,
- `7 : 8 b.test.js:6:11 › failing 2`,
- `7 : ${NEGATIVE_STATUS_MARK} 8 b.test.js:6:11 › failing 2 (Xms)`,
- `8 : 9 b.test.js:6:11 › failing 2 (retry #1)`,
- `8 : ${NEGATIVE_STATUS_MARK} 9 b.test.js:6:11 › failing 2 (retry #1) (Xms)`,
- `9 : 10 b.test.js:9:12 › skipped 2`,
- `9 : - 10 b.test.js:9:12 › skipped 2`
+ `#0 : 1 a.test.js:3:11 › math 1`,
+ `#0 : ${POSITIVE_STATUS_MARK} 1 a.test.js:3:11 › math 1 (Xms)`,
+ `#1 : 2 a.test.js:6:11 › failing 1`,
+ `#1 : ${NEGATIVE_STATUS_MARK} 2 a.test.js:6:11 › failing 1 (Xms)`,
+ `#2 : 3 a.test.js:6:11 › failing 1 (retry #1)`,
+ `#2 : ${NEGATIVE_STATUS_MARK} 3 a.test.js:6:11 › failing 1 (retry #1) (Xms)`,
+ `#3 : 4 a.test.js:9:11 › flaky 1`,
+ `#3 : ${NEGATIVE_STATUS_MARK} 4 a.test.js:9:11 › flaky 1 (Xms)`,
+ `#4 : 5 a.test.js:9:11 › flaky 1 (retry #1)`,
+ `#4 : ${POSITIVE_STATUS_MARK} 5 a.test.js:9:11 › flaky 1 (retry #1) (Xms)`,
+ `#5 : 6 a.test.js:12:12 › skipped 1`,
+ `#5 : - 6 a.test.js:12:12 › skipped 1`,
+ `#6 : 7 b.test.js:3:11 › math 2`,
+ `#6 : ${POSITIVE_STATUS_MARK} 7 b.test.js:3:11 › math 2 (Xms)`,
+ `#7 : 8 b.test.js:6:11 › failing 2`,
+ `#7 : ${NEGATIVE_STATUS_MARK} 8 b.test.js:6:11 › failing 2 (Xms)`,
+ `#8 : 9 b.test.js:6:11 › failing 2 (retry #1)`,
+ `#8 : ${NEGATIVE_STATUS_MARK} 9 b.test.js:6:11 › failing 2 (retry #1) (Xms)`,
+ `#9 : 10 b.test.js:9:12 › skipped 2`,
+ `#9 : - 10 b.test.js:9:12 › skipped 2`,
+ `#10 : 11 c.test.js:3:11 › math 3`,
+ `#10 : ${POSITIVE_STATUS_MARK} 11 c.test.js:3:11 › math 3 (Xms)`,
+ `#11 : 12 c.test.js:6:11 › flaky 2`,
+ `#11 : ${NEGATIVE_STATUS_MARK} 12 c.test.js:6:11 › flaky 2 (Xms)`,
+ `#12 : 13 c.test.js:6:11 › flaky 2 (retry #1)`,
+ `#12 : ${POSITIVE_STATUS_MARK} 13 c.test.js:6:11 › flaky 2 (retry #1) (Xms)`,
+ `#13 : 14 c.test.js:9:12 › skipped 3`,
+ `#13 : - 14 c.test.js:9:12 › skipped 3`,
]);
});
diff --git a/tests/playwright-test/reporter-list.spec.ts b/tests/playwright-test/reporter-list.spec.ts
index 489e97feb7..c57190a5d7 100644
--- a/tests/playwright-test/reporter-list.spec.ts
+++ b/tests/playwright-test/reporter-list.spec.ts
@@ -72,22 +72,22 @@ for (const useIntermediateMergeReport of [false, true] as const) {
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_LIST_PRINT_STEPS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
const text = result.output;
- const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
+ const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
lines.pop(); // Remove last item that contains [v] and time in ms.
expect(lines).toEqual([
- '0 : 1 a.test.ts:3:15 › passes',
- '1 : 1.1 passes › outer 1.0',
- '2 : 1.2 passes › outer 1.0 › inner 1.1',
- '2 : 1.2 passes › outer 1.0 › inner 1.1 (Xms)',
- '3 : 1.3 passes › outer 1.0 › inner 1.2',
- '3 : 1.3 passes › outer 1.0 › inner 1.2 (Xms)',
- '1 : 1.1 passes › outer 1.0 (Xms)',
- '4 : 1.4 passes › outer 2.0',
- '5 : 1.5 passes › outer 2.0 › inner 2.1',
- '5 : 1.5 passes › outer 2.0 › inner 2.1 (Xms)',
- '6 : 1.6 passes › outer 2.0 › inner 2.2',
- '6 : 1.6 passes › outer 2.0 › inner 2.2 (Xms)',
- '4 : 1.4 passes › outer 2.0 (Xms)',
+ '#0 : 1 a.test.ts:3:15 › passes',
+ '#1 : 1.1 passes › outer 1.0',
+ '#2 : 1.2 passes › outer 1.0 › inner 1.1',
+ '#2 : 1.2 passes › outer 1.0 › inner 1.1 (Xms)',
+ '#3 : 1.3 passes › outer 1.0 › inner 1.2',
+ '#3 : 1.3 passes › outer 1.0 › inner 1.2 (Xms)',
+ '#1 : 1.1 passes › outer 1.0 (Xms)',
+ '#4 : 1.4 passes › outer 2.0',
+ '#5 : 1.5 passes › outer 2.0 › inner 2.1',
+ '#5 : 1.5 passes › outer 2.0 › inner 2.1 (Xms)',
+ '#6 : 1.6 passes › outer 2.0 › inner 2.2',
+ '#6 : 1.6 passes › outer 2.0 › inner 2.2 (Xms)',
+ '#4 : 1.4 passes › outer 2.0 (Xms)',
]);
});
@@ -107,22 +107,51 @@ for (const useIntermediateMergeReport of [false, true] as const) {
});`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
const text = result.output;
- const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
+ const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
lines.pop(); // Remove last item that contains [v] and time in ms.
expect(lines).toEqual([
- '0 : 1 a.test.ts:3:11 › passes',
- '0 : 1 a.test.ts:4:20 › passes › outer 1.0',
- '0 : 1 a.test.ts:5:22 › passes › outer 1.0 › inner 1.1',
- '0 : 1 a.test.ts:4:20 › passes › outer 1.0',
- '0 : 1 a.test.ts:6:22 › passes › outer 1.0 › inner 1.2',
- '0 : 1 a.test.ts:4:20 › passes › outer 1.0',
- '0 : 1 a.test.ts:3:11 › passes',
- '0 : 1 a.test.ts:8:20 › passes › outer 2.0',
- '0 : 1 a.test.ts:9:22 › passes › outer 2.0 › inner 2.1',
- '0 : 1 a.test.ts:8:20 › passes › outer 2.0',
- '0 : 1 a.test.ts:10:22 › passes › outer 2.0 › inner 2.2',
- '0 : 1 a.test.ts:8:20 › passes › outer 2.0',
- '0 : 1 a.test.ts:3:11 › passes',
+ '#0 : 1 a.test.ts:3:11 › passes',
+ '#0 : 1 a.test.ts:4:20 › passes › outer 1.0',
+ '#0 : 1 a.test.ts:5:22 › passes › outer 1.0 › inner 1.1',
+ '#0 : 1 a.test.ts:4:20 › passes › outer 1.0',
+ '#0 : 1 a.test.ts:6:22 › passes › outer 1.0 › inner 1.2',
+ '#0 : 1 a.test.ts:4:20 › passes › outer 1.0',
+ '#0 : 1 a.test.ts:3:11 › passes',
+ '#0 : 1 a.test.ts:8:20 › passes › outer 2.0',
+ '#0 : 1 a.test.ts:9:22 › passes › outer 2.0 › inner 2.1',
+ '#0 : 1 a.test.ts:8:20 › passes › outer 2.0',
+ '#0 : 1 a.test.ts:10:22 › passes › outer 2.0 › inner 2.2',
+ '#0 : 1 a.test.ts:8:20 › passes › outer 2.0',
+ '#0 : 1 a.test.ts:3:11 › passes',
+ ]);
+ });
+
+ test('render steps in non-TTY mode', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'a.test.ts': `
+ import { test, expect } from '@playwright/test';
+ test('passes', async ({}) => {
+ await test.step('outer 1.0', async () => {
+ await test.step('inner 1.1', async () => {});
+ await test.step('inner 1.2', async () => {});
+ });
+ await test.step('outer 2.0', async () => {
+ await test.step('inner 2.1', async () => {});
+ await test.step('inner 2.2', async () => {});
+ });
+ });
+ `,
+ }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_LIST_PRINT_STEPS: '1' });
+ const text = result.output;
+ const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
+ expect(lines).toEqual([
+ '#0 : 1.1 a.test.ts:5:26 › passes › outer 1.0 › inner 1.1 (Xms)',
+ '#1 : 1.2 a.test.ts:6:26 › passes › outer 1.0 › inner 1.2 (Xms)',
+ '#2 : 1.3 a.test.ts:4:24 › passes › outer 1.0 (Xms)',
+ '#3 : 1.4 a.test.ts:9:26 › passes › outer 2.0 › inner 2.1 (Xms)',
+ '#4 : 1.5 a.test.ts:10:26 › passes › outer 2.0 › inner 2.2 (Xms)',
+ '#5 : 1.6 a.test.ts:8:24 › passes › outer 2.0 (Xms)',
+ `#6 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:15 › passes (Xms)`,
]);
});
@@ -156,13 +185,13 @@ for (const useIntermediateMergeReport of [false, true] as const) {
`,
}, { reporter: 'list', retries: '1' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
const text = result.output;
- const lines = text.split('\n').filter(l => l.startsWith('0 :') || l.startsWith('1 :')).map(l => l.replace(/\d+(\.\d+)?m?s/, 'XXms'));
+ const lines = text.split('\n').filter(l => l.startsWith('#0 :') || l.startsWith('#1 :')).map(l => l.replace(/\d+(\.\d+)?m?s/, 'XXms'));
expect(lines).toEqual([
- `0 : 1 a.test.ts:3:15 › flaky`,
- `0 : ${NEGATIVE_STATUS_MARK} 1 a.test.ts:3:15 › flaky (XXms)`,
- `1 : 2 a.test.ts:3:15 › flaky (retry #1)`,
- `1 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:3:15 › flaky (retry #1) (XXms)`,
+ `#0 : 1 a.test.ts:3:15 › flaky`,
+ `#0 : ${NEGATIVE_STATUS_MARK} 1 a.test.ts:3:15 › flaky (XXms)`,
+ `#1 : 2 a.test.ts:3:15 › flaky (retry #1)`,
+ `#1 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:3:15 › flaky (retry #1) (XXms)`,
]);
});
From f4399f7f062315034a52c2ae395e00979b7cc54d Mon Sep 17 00:00:00 2001
From: Dmitry Gozman
Date: Wed, 17 Jul 2024 07:08:43 -0700
Subject: [PATCH 072/376] fix(toHaveScreenshot): sanitize attachment names and
paths (#31712)
... unless an array of file-system-friendly parts is provided.
Motivation: attachment name is used as a file system path when
downloading attachments, so we keep them fs-friendly.
References #30693.
---
.../src/matchers/toMatchSnapshot.ts | 108 +++++++++---------
tests/playwright-test/golden.spec.ts | 8 +-
.../to-have-screenshot.spec.ts | 46 ++++++++
3 files changed, 105 insertions(+), 57 deletions(-)
diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts
index 7a3242100e..a533f13b48 100644
--- a/packages/playwright/src/matchers/toMatchSnapshot.ts
+++ b/packages/playwright/src/matchers/toMatchSnapshot.ts
@@ -75,10 +75,10 @@ const NonConfigProperties: (keyof ToHaveScreenshotOptions)[] = [
class SnapshotHelper {
readonly testInfo: TestInfoImpl;
- readonly outputBaseName: string;
+ readonly attachmentBaseName: string;
readonly legacyExpectedPath: string;
readonly previousPath: string;
- readonly snapshotPath: string;
+ readonly expectedPath: string;
readonly actualPath: string;
readonly diffPath: string;
readonly mimeType: string;
@@ -117,40 +117,42 @@ class SnapshotHelper {
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
}
- // Consider the use case below. We should save actual to different paths.
- //
- // expect.toMatchSnapshot('a.png')
- // // noop
- // expect.toMatchSnapshot('a.png')
-
- let inputPathSegments: string[];
+ let expectedPathSegments: string[];
+ let outputBasePath: string;
if (!name) {
+ // Consider the use case below. We should save actual to different paths.
+ // Therefore we auto-increment |anonymousSnapshotIndex|.
+ //
+ // expect.toMatchSnapshot('a.png')
+ // // noop
+ // expect.toMatchSnapshot('a.png')
const fullTitleWithoutSpec = [
...testInfo.titlePath.slice(1),
++snapshotNames.anonymousSnapshotIndex,
].join(' ');
- inputPathSegments = [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension];
+ // Note: expected path must not ever change for backwards compatibility.
+ expectedPathSegments = [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension];
// Trim the output file paths more aggressively to avoid hitting Windows filesystem limits.
- this.outputBaseName = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength)) + '.' + anonymousSnapshotExtension;
+ const sanitizedName = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength)) + '.' + anonymousSnapshotExtension;
+ outputBasePath = testInfo._getOutputPath(sanitizedName);
+ this.attachmentBaseName = sanitizedName;
} else {
- // We intentionally do not sanitize user-provided array of segments, but for backwards
- // compatibility we do sanitize the name if it is a single string.
- // See https://github.com/microsoft/playwright/pull/9156
- inputPathSegments = Array.isArray(name) ? name : [sanitizeFilePathBeforeExtension(name)];
- const joinedName = Array.isArray(name) ? name.join(path.sep) : name;
+ // We intentionally do not sanitize user-provided array of segments, assuming
+ // it is a file system path. See https://github.com/microsoft/playwright/pull/9156.
+ // Note: expected path must not ever change for backwards compatibility.
+ expectedPathSegments = Array.isArray(name) ? name : [sanitizeFilePathBeforeExtension(name)];
+ const joinedName = Array.isArray(name) ? name.join(path.sep) : sanitizeFilePathBeforeExtension(trimLongString(name, windowsFilesystemFriendlyLength));
snapshotNames.namedSnapshotIndex[joinedName] = (snapshotNames.namedSnapshotIndex[joinedName] || 0) + 1;
const index = snapshotNames.namedSnapshotIndex[joinedName];
- if (index > 1)
- this.outputBaseName = addSuffixToFilePath(joinedName, `-${index - 1}`);
- else
- this.outputBaseName = joinedName;
+ const sanitizedName = index > 1 ? addSuffixToFilePath(joinedName, `-${index - 1}`) : joinedName;
+ outputBasePath = testInfo._getOutputPath(sanitizedName);
+ this.attachmentBaseName = sanitizedName;
}
- this.snapshotPath = testInfo.snapshotPath(...inputPathSegments);
- const outputFile = testInfo._getOutputPath(sanitizeFilePathBeforeExtension(this.outputBaseName));
- this.legacyExpectedPath = addSuffixToFilePath(outputFile, '-expected');
- this.previousPath = addSuffixToFilePath(outputFile, '-previous');
- this.actualPath = addSuffixToFilePath(outputFile, '-actual');
- this.diffPath = addSuffixToFilePath(outputFile, '-diff');
+ this.expectedPath = testInfo.snapshotPath(...expectedPathSegments);
+ this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
+ this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
+ this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');
+ this.diffPath = addSuffixToFilePath(outputBasePath, '-diff');
const filteredConfigOptions = { ...configOptions };
for (const prop of NonConfigProperties)
@@ -176,7 +178,7 @@ class SnapshotHelper {
this.locator = locator;
this.updateSnapshots = testInfo.config.updateSnapshots;
- this.mimeType = mime.getType(path.basename(this.snapshotPath)) ?? 'application/octet-string';
+ this.mimeType = mime.getType(path.basename(this.expectedPath)) ?? 'application/octet-string';
this.comparator = getComparator(this.mimeType);
this.testInfo = testInfo;
@@ -186,7 +188,7 @@ class SnapshotHelper {
createMatcherResult(message: string, pass: boolean, log?: string[]): ImageMatcherResult {
const unfiltered: ImageMatcherResult = {
name: this.matcherName,
- expected: this.snapshotPath,
+ expected: this.expectedPath,
actual: this.actualPath,
diff: this.diffPath,
pass,
@@ -198,7 +200,7 @@ class SnapshotHelper {
handleMissingNegated(): ImageMatcherResult {
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
- const message = `A snapshot doesn't exist at ${this.snapshotPath}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
+ const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
// NOTE: 'isNot' matcher implies inversed value.
return this.createMatcherResult(message, true);
}
@@ -221,11 +223,11 @@ class SnapshotHelper {
handleMissing(actual: Buffer | string): ImageMatcherResult {
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
if (isWriteMissingMode) {
- writeFileSync(this.snapshotPath, actual);
+ writeFileSync(this.expectedPath, actual);
writeFileSync(this.actualPath, actual);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
}
- const message = `A snapshot doesn't exist at ${this.snapshotPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
+ const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
if (this.updateSnapshots === 'all') {
/* eslint-disable no-console */
console.log(message);
@@ -258,22 +260,22 @@ class SnapshotHelper {
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
// so that one can upload `test-results/` directory and have all the data inside.
writeFileSync(this.legacyExpectedPath, expected);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-expected'), contentType: this.mimeType, path: this.snapshotPath });
- output.push(`\nExpected: ${colors.yellow(this.snapshotPath)}`);
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
+ output.push(`\nExpected: ${colors.yellow(this.expectedPath)}`);
}
if (previous !== undefined) {
writeFileSync(this.previousPath, previous);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
output.push(`Previous: ${colors.yellow(this.previousPath)}`);
}
if (actual !== undefined) {
writeFileSync(this.actualPath, actual);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
output.push(`Received: ${colors.yellow(this.actualPath)}`);
}
if (diff !== undefined) {
writeFileSync(this.diffPath, diff);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
output.push(` Diff: ${colors.yellow(this.diffPath)}`);
}
@@ -311,25 +313,25 @@ export function toMatchSnapshot(
configOptions, nameOrOptions, optOptions);
if (this.isNot) {
- if (!fs.existsSync(helper.snapshotPath))
+ if (!fs.existsSync(helper.expectedPath))
return helper.handleMissingNegated();
- const isDifferent = !!helper.comparator(received, fs.readFileSync(helper.snapshotPath), helper.options);
+ const isDifferent = !!helper.comparator(received, fs.readFileSync(helper.expectedPath), helper.options);
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
}
- if (!fs.existsSync(helper.snapshotPath))
+ if (!fs.existsSync(helper.expectedPath))
return helper.handleMissing(received);
- const expected = fs.readFileSync(helper.snapshotPath);
+ const expected = fs.readFileSync(helper.expectedPath);
const result = helper.comparator(received, expected, helper.options);
if (!result)
return helper.handleMatching();
if (helper.updateSnapshots === 'all') {
- writeFileSync(helper.snapshotPath, received);
+ writeFileSync(helper.expectedPath, received);
/* eslint-disable no-console */
- console.log(helper.snapshotPath + ' does not match, writing actual.');
- return helper.createMatcherResult(helper.snapshotPath + ' running with --update-snapshots, writing actual.', true);
+ console.log(helper.expectedPath + ' does not match, writing actual.');
+ return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
}
return helper.handleDifferent(received, expected, undefined, result.diff, result.errorMessage, undefined);
@@ -364,8 +366,8 @@ export async function toHaveScreenshot(
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as Locator];
const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
const helper = new SnapshotHelper(testInfo, 'toHaveScreenshot', locator, 'png', configOptions, nameOrOptions, optOptions);
- if (!helper.snapshotPath.toLowerCase().endsWith('.png'))
- throw new Error(`Screenshot name "${path.basename(helper.snapshotPath)}" must have '.png' extension`);
+ if (!helper.expectedPath.toLowerCase().endsWith('.png'))
+ throw new Error(`Screenshot name "${path.basename(helper.expectedPath)}" must have '.png' extension`);
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
const style = await loadScreenshotStyles(helper.options.stylePath);
const expectScreenshotOptions: ExpectScreenshotOptions = {
@@ -387,7 +389,7 @@ export async function toHaveScreenshot(
threshold: helper.options.threshold,
};
- const hasSnapshot = fs.existsSync(helper.snapshotPath);
+ const hasSnapshot = fs.existsSync(helper.expectedPath);
if (this.isNot) {
if (!hasSnapshot)
return helper.handleMissingNegated();
@@ -395,14 +397,14 @@ export async function toHaveScreenshot(
// Having `errorMessage` means we timed out while waiting
// for screenshots not to match, so screenshots
// are actually the same in the end.
- expectScreenshotOptions.expected = await fs.promises.readFile(helper.snapshotPath);
+ expectScreenshotOptions.expected = await fs.promises.readFile(helper.expectedPath);
const isDifferent = !(await page._expectScreenshot(expectScreenshotOptions)).errorMessage;
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
}
// Fast path: there's no screenshot and we don't intend to update it.
if (helper.updateSnapshots === 'none' && !hasSnapshot)
- return helper.createMatcherResult(`A snapshot doesn't exist at ${helper.snapshotPath}.`, false);
+ return helper.createMatcherResult(`A snapshot doesn't exist at ${helper.expectedPath}.`, false);
if (!hasSnapshot) {
// Regenerate a new screenshot by waiting until two screenshots are the same.
@@ -420,18 +422,18 @@ export async function toHaveScreenshot(
// - snapshot exists
// - regular matcher (i.e. not a `.not`)
// - perhaps an 'all' flag to update non-matching screenshots
- expectScreenshotOptions.expected = await fs.promises.readFile(helper.snapshotPath);
+ expectScreenshotOptions.expected = await fs.promises.readFile(helper.expectedPath);
const { actual, diff, errorMessage, log } = await page._expectScreenshot(expectScreenshotOptions);
if (!errorMessage)
return helper.handleMatching();
if (helper.updateSnapshots === 'all') {
- writeFileSync(helper.snapshotPath, actual!);
+ writeFileSync(helper.expectedPath, actual!);
writeFileSync(helper.actualPath, actual!);
/* eslint-disable no-console */
- console.log(helper.snapshotPath + ' is re-generated, writing actual.');
- return helper.createMatcherResult(helper.snapshotPath + ' running with --update-snapshots, writing actual.', true);
+ console.log(helper.expectedPath + ' is re-generated, writing actual.');
+ return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
}
return helper.handleDifferent(actual, expectScreenshotOptions.expected, undefined, diff, errorMessage, log);
diff --git a/tests/playwright-test/golden.spec.ts b/tests/playwright-test/golden.spec.ts
index 41cdb9da5a..205eeee7f0 100644
--- a/tests/playwright-test/golden.spec.ts
+++ b/tests/playwright-test/golden.spec.ts
@@ -145,7 +145,7 @@ test('should generate separate actual results for repeating names', async ({ run
{
'name': 'bar/baz-actual.txt',
'contentType': 'text/plain',
- 'path': 'test-results/a-is-a-test/bar-baz-actual.txt'
+ 'path': 'test-results/a-is-a-test/bar/baz-actual.txt'
},
{
'name': 'bar/baz-1-expected.txt',
@@ -155,7 +155,7 @@ test('should generate separate actual results for repeating names', async ({ run
{
'name': 'bar/baz-1-actual.txt',
'contentType': 'text/plain',
- 'path': 'test-results/a-is-a-test/bar-baz-1-actual.txt'
+ 'path': 'test-results/a-is-a-test/bar/baz-1-actual.txt'
}
]);
});
@@ -977,12 +977,12 @@ test('should attach expected/actual/diff with snapshot path', async ({ runInline
{
name: 'test/path/snapshot-actual.png',
contentType: 'image/png',
- path: 'a-is-a-test/test-path-snapshot-actual.png'
+ path: 'a-is-a-test/test/path/snapshot-actual.png'
},
{
name: 'test/path/snapshot-diff.png',
contentType: 'image/png',
- path: 'a-is-a-test/test-path-snapshot-diff.png'
+ path: 'a-is-a-test/test/path/snapshot-diff.png'
}
]);
});
diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts
index 07fb590375..39044e460a 100644
--- a/tests/playwright-test/to-have-screenshot.spec.ts
+++ b/tests/playwright-test/to-have-screenshot.spec.ts
@@ -1320,3 +1320,49 @@ function playwrightConfig(obj: any) {
`,
};
}
+
+test('should trim+sanitize attachment names and paths', async ({ runInlineTest }, testInfo) => {
+ const result = await runInlineTest({
+ ...playwrightConfig({
+ snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
+ }),
+ 'a.spec.js': `
+ const { test, expect } = require('@playwright/test');
+ test.afterEach(async ({}, testInfo) => {
+ console.log('## ' + JSON.stringify(testInfo.attachments));
+ });
+ const title = 'long '.repeat(30) + 'title';
+ test(title, async ({ page }) => {
+ await expect.soft(page).toHaveScreenshot();
+ const name = 'long '.repeat(30) + 'name.png';
+ await expect.soft(page).toHaveScreenshot(name);
+ await expect.soft(page).toHaveScreenshot(['dir', name]);
+ });
+ `
+ });
+
+ expect(result.exitCode).toBe(1);
+ const attachments = result.output.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0];
+ for (const attachment of attachments) {
+ attachment.path = attachment.path.replace(testInfo.outputDir, '').substring(1).replace(/\\/g, '/');
+ attachment.name = attachment.name.replace(/\\/g, '/');
+ }
+ expect(attachments).toEqual([
+ {
+ name: 'long-long-long-long-long-l-852e1-long-long-long-long-title-1-actual.png',
+ contentType: 'image/png',
+ path: 'test-results/a-long-long-long-long-long-abd51-g-long-long-long-long-title/long-long-long-long-long-l-852e1-long-long-long-long-title-1-actual.png',
+ },
+ {
+ name: 'long-long-long-long-long-l-6bf1e-ong-long-long-long-name-actual.png',
+ contentType: 'image/png',
+ path: 'test-results/a-long-long-long-long-long-abd51-g-long-long-long-long-title/long-long-long-long-long-l-6bf1e-ong-long-long-long-name-actual.png',
+ },
+ {
+ name: 'dir/long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long name-actual.png',
+ contentType: 'image/png',
+ path: 'test-results/a-long-long-long-long-long-abd51-g-long-long-long-long-title/dir/long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long name-actual.png',
+ },
+ ]);
+});
+
From 3f15fe8518b1c0bfded61f902300a267b44a4530 Mon Sep 17 00:00:00 2001
From: Yury Semikhatsky
Date: Wed, 17 Jul 2024 09:30:49 -0700
Subject: [PATCH 073/376] feat(reporter): links in attachment names,
attachments name only (#31714)
* Allow calling `test.info().attach('My text');` without options (no
path nor body).
* Highlight links in attachment names:
Fixes https://github.com/microsoft/playwright/issues/31284
---
packages/html-reporter/src/links.tsx | 2 +-
.../html-reporter/src/testCaseView.spec.tsx | 12 +++++++++++-
packages/playwright/src/reporters/base.ts | 2 +-
packages/playwright/src/util.ts | 2 ++
.../reporter-attachment.spec.ts | 19 +++++++++++++++++--
5 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx
index 4ddaf20a91..42b9f9b5b9 100644
--- a/packages/html-reporter/src/links.tsx
+++ b/packages/html-reporter/src/links.tsx
@@ -78,7 +78,7 @@ export const AttachmentLink: React.FunctionComponent<{
return
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
{attachment.path && {linkName || attachment.name}}
- {attachment.body && {linkifyText(attachment.name)}}
+ {!attachment.path && {linkifyText(attachment.name)}}
} loadChildren={attachment.body ? () => {
return [
{linkifyText(attachment.body!)}
];
} : undefined} depth={0} style={{ lineHeight: '32px' }}>;
diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx
index d73407582d..624a93805f 100644
--- a/packages/html-reporter/src/testCaseView.spec.tsx
+++ b/packages/html-reporter/src/testCaseView.spec.tsx
@@ -132,6 +132,9 @@ const resultWithAttachment: TestResult = {
name: 'first attachment',
body: 'The body with https://playwright.dev/docs/intro link and https://github.com/microsoft/playwright/issues/31284.',
contentType: 'text/plain'
+ }, {
+ name: 'attachment with inline link https://github.com/microsoft/playwright/issues/31284',
+ contentType: 'text/plain'
}],
status: 'passed',
};
@@ -157,4 +160,11 @@ test('should correctly render links in attachments', async ({ mount }) => {
await expect(body).toBeVisible();
await expect(body.locator('a').filter({ hasText: 'playwright.dev' })).toHaveAttribute('href', 'https://playwright.dev/docs/intro');
await expect(body.locator('a').filter({ hasText: 'github.com' })).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
-});
\ No newline at end of file
+});
+
+test('should correctly render links in attachment name', async ({ mount }) => {
+ const component = await mount();
+ const link = component.getByText('attachment with inline link').locator('a');
+ await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
+ await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284');
+});
diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts
index e4575a8627..c9ce2f7bcd 100644
--- a/packages/playwright/src/reporters/base.ts
+++ b/packages/playwright/src/reporters/base.ts
@@ -319,7 +319,7 @@ export function formatFailure(config: FullConfig, test: TestCase, options: {inde
if (includeAttachments) {
for (let i = 0; i < result.attachments.length; ++i) {
const attachment = result.attachments[i];
- const hasPrintableContent = attachment.contentType.startsWith('text/') && attachment.body;
+ const hasPrintableContent = attachment.contentType.startsWith('text/');
if (!attachment.path && !hasPrintableContent)
continue;
resultLines.push('');
diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts
index d6cabd07c6..a4ddce7a3b 100644
--- a/packages/playwright/src/util.ts
+++ b/packages/playwright/src/util.ts
@@ -256,6 +256,8 @@ export function resolveReporterOutputPath(defaultValue: string, configDir: strin
}
export async function normalizeAndSaveAttachment(outputPath: string, name: string, options: { path?: string, body?: string | Buffer, contentType?: string } = {}): Promise<{ name: string; path?: string | undefined; body?: Buffer | undefined; contentType: string; }> {
+ if (options.path === undefined && options.body === undefined)
+ return { name, contentType: 'text/plain' };
if ((options.path !== undefined ? 1 : 0) + (options.body !== undefined ? 1 : 0) !== 1)
throw new Error(`Exactly one of "path" and "body" must be specified`);
if (options.path !== undefined) {
diff --git a/tests/playwright-test/reporter-attachment.spec.ts b/tests/playwright-test/reporter-attachment.spec.ts
index fd938be437..50d6d1ee0c 100644
--- a/tests/playwright-test/reporter-attachment.spec.ts
+++ b/tests/playwright-test/reporter-attachment.spec.ts
@@ -99,8 +99,8 @@ test(`testInfo.attach errors`, async ({ runInlineTest }) => {
const text = result.output.replace(/\\/g, '/');
expect(text).toMatch(/Error: ENOENT: no such file or directory, copyfile '.*foo.txt.*'/);
expect(text).toContain(`Exactly one of "path" and "body" must be specified`);
- expect(result.passed).toBe(0);
- expect(result.failed).toBe(3);
+ expect(result.passed).toBe(1);
+ expect(result.failed).toBe(2);
expect(result.exitCode).toBe(1);
});
@@ -175,6 +175,21 @@ test(`testInfo.attach allow empty string body`, async ({ runInlineTest }) => {
expect(result.output).toMatch(/^.*attachment #1: name \(text\/plain\).*\n.*\n.*──────/gm);
});
+test(`testInfo.attach allow without options`, async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'a.test.ts': `
+ import { test, expect } from '@playwright/test';
+ test('success', async ({}, testInfo) => {
+ await testInfo.attach('Full name');
+ expect(0).toBe(1);
+ });
+ `,
+ });
+ expect(result.exitCode).toBe(1);
+ expect(result.failed).toBe(1);
+ expect(result.output).toMatch(/^.*attachment #1: Full name \(text\/plain\).*\n.*──────/gm);
+});
+
test(`testInfo.attach allow empty buffer body`, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
From 3cb41739a03f2e213b10d9ed32f17d6a4aab9f11 Mon Sep 17 00:00:00 2001
From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com>
Date: Wed, 17 Jul 2024 09:33:47 -0700
Subject: [PATCH 074/376] feat(firefox-beta): roll to r1457 (#31733)
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 1f00780f4f..d7abe551c1 100644
--- a/packages/playwright-core/browsers.json
+++ b/packages/playwright-core/browsers.json
@@ -21,9 +21,9 @@
},
{
"name": "firefox-beta",
- "revision": "1456",
+ "revision": "1457",
"installByDefault": false,
- "browserVersion": "128.0b3"
+ "browserVersion": "129.0b2"
},
{
"name": "webkit",
From e06481a33254fc7e8d94cf891dcdd0b5c7eb5acc Mon Sep 17 00:00:00 2001
From: Matt Kleinsmith <8968171+MattKleinsmith@users.noreply.github.com>
Date: Wed, 17 Jul 2024 11:45:48 -0700
Subject: [PATCH 075/376] fix(recorder): address custom context menus (#31634)
---
.../src/server/injected/recorder/recorder.ts | 22 ++++++++
tests/library/inspector/cli-codegen-3.spec.ts | 54 +++++++++++++++++++
tests/library/inspector/inspectorTest.ts | 6 +--
3 files changed, 79 insertions(+), 3 deletions(-)
diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts
index 4cfa512a4f..116fca63bc 100644
--- a/packages/playwright-core/src/server/injected/recorder/recorder.ts
+++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts
@@ -259,6 +259,28 @@ class RecordActionTool implements RecorderTool {
});
}
+ onContextMenu(event: MouseEvent) {
+ // the 'contextmenu' event is triggered by a right-click or equivalent action,
+ // and it prevents the click event from firing for that action, so we always
+ // convert 'contextmenu' into a right-click.
+ if (this._shouldIgnoreMouseEvent(event))
+ return;
+ if (this._actionInProgress(event))
+ return;
+ if (this._consumedDueToNoModel(event, this._hoveredModel))
+ return;
+
+ this._performAction({
+ name: 'click',
+ selector: this._hoveredModel!.selector,
+ position: positionForEvent(event),
+ signals: [],
+ button: 'right',
+ modifiers: 0,
+ clickCount: 0
+ });
+ }
+
onPointerDown(event: PointerEvent) {
if (this._shouldIgnoreMouseEvent(event))
return;
diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts
index 2c0fff974a..8ad14c87b9 100644
--- a/tests/library/inspector/cli-codegen-3.spec.ts
+++ b/tests/library/inspector/cli-codegen-3.spec.ts
@@ -559,6 +559,60 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
]);
});
+ test('should consume contextmenu events, despite a custom context menu', async ({ page, openRecorder }) => {
+ const recorder = await openRecorder();
+
+ await recorder.setContentAndWait(`
+
+
+
+
+
+
+ `);
+
+ await recorder.hoverOverElement('button');
+ expect(await page.evaluate('log')).toEqual(['button: pointermove', 'button: mousemove']);
+
+ const [message] = await Promise.all([
+ page.waitForEvent('console', msg => msg.type() !== 'error'),
+ recorder.waitForOutput('JavaScript', `button: 'right'`),
+ recorder.trustedClick({ button: 'right' }),
+ ]);
+ expect(message.text()).toBe('right-clicked');
+ expect(await page.evaluate('log')).toEqual([
+ // hover
+ 'button: pointermove',
+ 'button: mousemove',
+ // trusted right click
+ 'button: pointerup',
+ 'button: pointermove',
+ 'button: mousemove',
+ 'button: pointerdown',
+ 'button: mousedown',
+ 'button: contextmenu',
+ 'menu: pointerup',
+ 'menu: mouseup'
+ ]);
+ });
+
test('should assert value', async ({ openRecorder }) => {
const recorder = await openRecorder();
diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts
index 1d5e30825a..6ebbc1fdd1 100644
--- a/tests/library/inspector/inspectorTest.ts
+++ b/tests/library/inspector/inspectorTest.ts
@@ -186,9 +186,9 @@ class Recorder {
await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
}
- async trustedClick() {
- await this.page.mouse.down();
- await this.page.mouse.up();
+ async trustedClick(options?: { button?: 'left' | 'right' | 'middle' }) {
+ await this.page.mouse.down(options);
+ await this.page.mouse.up(options);
}
async focusElement(selector: string): Promise {
From 6491e5b415a7abc72706450f463286277db7fd1f Mon Sep 17 00:00:00 2001
From: Dmitry Gozman
Date: Thu, 18 Jul 2024 00:19:08 -0700
Subject: [PATCH 076/376] chore: deprecate/remove noWaitAfter from some actions
(#31739)
The following actions keep `noWaitAfter` option: `click`, `selectOption`
and `press`.
All other actions that used to have `noWaitAfter` now behave like it was
set to true, not waiting for follow-up navigations. In the docs, this
option is marked as completely ignored.
A small logic change was made to compensate for this behavior: when
waiting for the `hitTargetInterceptor`, we now race it against
navigations to avoid stalling when navigation stalls. Previously,
waiting for the interceptor was disabled when `noWaitAfter` was passed,
and since it's impossible to pass this option now, we mitigate by never
stalling instead.
Fixes #31469.
---
docs/src/api/class-elementhandle.md | 25 +-
docs/src/api/class-filechooser.md | 2 +-
docs/src/api/class-frame.md | 26 +-
docs/src/api/class-locator.md | 31 +--
docs/src/api/class-page.md | 27 +-
docs/src/api/params.md | 7 +
.../playwright-core/src/protocol/validator.ts | 17 --
packages/playwright-core/src/server/dom.ts | 163 +++++------
packages/playwright-core/src/server/frames.ts | 35 +--
packages/playwright-core/src/server/types.ts | 8 +-
packages/playwright-core/types/types.d.ts | 253 +++++++-----------
packages/protocol/src/channels.ts | 34 ---
packages/protocol/src/protocol.yml | 17 --
tests/library/tap.spec.ts | 27 +-
tests/page/locator-misc-1.spec.ts | 10 -
tests/page/page-autowaiting-basic.spec.ts | 4 +-
tests/page/page-mouse.spec.ts | 10 -
17 files changed, 258 insertions(+), 438 deletions(-)
diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md
index 24b8bb2b6f..1793798c8c 100644
--- a/docs/src/api/class-elementhandle.md
+++ b/docs/src/api/class-elementhandle.md
@@ -164,7 +164,6 @@ This method checks the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
-1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now checked. If not, this method throws.
If the element is detached from the DOM at any moment during the action, this method throws.
@@ -178,7 +177,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: ElementHandle.check.force = %%-input-force-%%
* since: v1.8
-### option: ElementHandle.check.noWaitAfter = %%-input-no-wait-after-%%
+### option: ElementHandle.check.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.8
### option: ElementHandle.check.timeout = %%-input-timeout-%%
@@ -251,8 +250,6 @@ This method double clicks the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to double click in the center of the element, or the specified [`option: position`].
-1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set. Note that
- if the first click of the `dblclick()` triggers a navigation event, this method will throw.
If the element is detached from the DOM at any moment during the action, this method throws.
@@ -278,7 +275,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: ElementHandle.dblclick.force = %%-input-force-%%
* since: v1.8
-### option: ElementHandle.dblclick.noWaitAfter = %%-input-no-wait-after-%%
+### option: ElementHandle.dblclick.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.8
### option: ElementHandle.dblclick.timeout = %%-input-timeout-%%
@@ -537,7 +534,7 @@ Value to set for the ``, `