diff --git a/.eslintrc.js b/.eslintrc.js index a116a37036..e71a4ffd09 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -115,7 +115,7 @@ module.exports = { "@typescript-eslint/type-annotation-spacing": 2, // file whitespace - "no-multiple-empty-lines": [2, {"max": 2}], + "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 0}], "no-mixed-spaces-and-tabs": 2, "no-trailing-spaces": 2, "linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ], @@ -123,6 +123,7 @@ module.exports = { "key-spacing": [2, { "beforeColon": false }], + "eol-last": 2, // copyright "notice/notice": [2, { diff --git a/.github/workflows/tests_bidi.yml b/.github/workflows/tests_bidi.yml index db54550a4c..6be824869a 100644 --- a/.github/workflows/tests_bidi.yml +++ b/.github/workflows/tests_bidi.yml @@ -48,6 +48,23 @@ jobs: if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: csv-report + name: csv-report-${{ matrix.channel }} path: test-results/report.csv retention-days: 7 + + - name: Azure Login + if: ${{ !cancelled() && github.ref == 'refs/heads/main' }} + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_BLOB_REPORTS_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_BLOB_REPORTS_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_BLOB_REPORTS_SUBSCRIPTION_ID }} + + - name: Upload report.csv to Azure + if: ${{ !cancelled() && github.ref == 'refs/heads/main' }} + run: | + REPORT_DIR='bidi-reports' + azcopy cp "./test-results/report.csv" "https://mspwblobreport.blob.core.windows.net/\$web/$REPORT_DIR/${{ matrix.channel }}.csv" + echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/${{ matrix.channel }}.csv" + env: + AZCOPY_AUTO_LOGIN_TYPE: AZCLI diff --git a/README.md b/README.md index 913aac3269..7d100113c5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-132.0.6834.46-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-132.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-132.0.6834.57-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-132.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 132.0.6834.46 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 132.0.6834.57 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 132.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index e93d02d9d8..38a3546e41 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -1717,16 +1717,21 @@ var banana = await page.GetByRole(AriaRole.Listitem).Nth(2); Creates a locator matching all elements that match one or both of the two locators. -Note that when both locators match something, the resulting locator will have multiple matches and violate [locator strictness](../locators.md#strictness) guidelines. +Note that when both locators match something, the resulting locator will have multiple matches, potentially causing a [locator strictness](../locators.md#strictness) violation. **Usage** Consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly. +:::note +If both "New email" button and security dialog appear on screen, the "or" locator will match both of them, +possibly throwing the ["strict mode violation" error](../locators.md#strictness). In this case, you can use [`method: Locator.first`] to only match one of them. +::: + ```js const newEmail = page.getByRole('button', { name: 'New' }); const dialog = page.getByText('Confirm security settings'); -await expect(newEmail.or(dialog)).toBeVisible(); +await expect(newEmail.or(dialog).first()).toBeVisible(); if (await dialog.isVisible()) await page.getByRole('button', { name: 'Dismiss' }).click(); await newEmail.click(); @@ -1735,7 +1740,7 @@ await newEmail.click(); ```java Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New")); Locator dialog = page.getByText("Confirm security settings"); -assertThat(newEmail.or(dialog)).isVisible(); +assertThat(newEmail.or(dialog).first()).isVisible(); if (dialog.isVisible()) page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click(); newEmail.click(); @@ -1744,7 +1749,7 @@ newEmail.click(); ```python async new_email = page.get_by_role("button", name="New") dialog = page.get_by_text("Confirm security settings") -await expect(new_email.or_(dialog)).to_be_visible() +await expect(new_email.or_(dialog).first).to_be_visible() if (await dialog.is_visible()): await page.get_by_role("button", name="Dismiss").click() await new_email.click() @@ -1753,7 +1758,7 @@ await new_email.click() ```python sync new_email = page.get_by_role("button", name="New") dialog = page.get_by_text("Confirm security settings") -expect(new_email.or_(dialog)).to_be_visible() +expect(new_email.or_(dialog).first).to_be_visible() if (dialog.is_visible()): page.get_by_role("button", name="Dismiss").click() new_email.click() @@ -1762,7 +1767,7 @@ new_email.click() ```csharp var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" }); var dialog = page.GetByText("Confirm security settings"); -await Expect(newEmail.Or(dialog)).ToBeVisibleAsync(); +await Expect(newEmail.Or(dialog).First).ToBeVisibleAsync(); if (await dialog.IsVisibleAsync()) await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).ClickAsync(); await newEmail.ClickAsync(); diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 7e61e1b3a5..c2adf3afc5 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1217,6 +1217,56 @@ Expected accessible description. * since: v1.44 +## async method: LocatorAssertions.toHaveAccessibleErrorMessage +* since: v1.50 +* langs: + - alias-java: hasAccessibleErrorMessage + +Ensures the [Locator] points to an element with a given [aria errormessage](https://w3c.github.io/aria/#aria-errormessage). + +**Usage** + +```js +const locator = page.getByTestId('username-input'); +await expect(locator).toHaveAccessibleErrorMessage('Username is required.'); +``` + +```java +Locator locator = page.getByTestId("username-input"); +assertThat(locator).hasAccessibleErrorMessage("Username is required."); +``` + +```python async +locator = page.get_by_test_id("username-input") +await expect(locator).to_have_accessible_error_message("Username is required.") +``` + +```python sync +locator = page.get_by_test_id("username-input") +expect(locator).to_have_accessible_error_message("Username is required.") +``` + +```csharp +var locator = Page.GetByTestId("username-input"); +await Expect(locator).ToHaveAccessibleErrorMessageAsync("Username is required."); +``` + +### param: LocatorAssertions.toHaveAccessibleErrorMessage.errorMessage +* since: v1.50 +- `errorMessage` <[string]|[RegExp]> + +Expected accessible error message. + +### option: LocatorAssertions.toHaveAccessibleErrorMessage.timeout = %%-js-assertions-timeout-%% +* since: v1.50 + +### option: LocatorAssertions.toHaveAccessibleErrorMessage.timeout = %%-csharp-java-python-assertions-timeout-%% +* since: v1.50 + +### option: LocatorAssertions.toHaveAccessibleErrorMessage.ignoreCase = %%-assertions-ignore-case-%% +* since: v1.50 + + ## async method: LocatorAssertions.toHaveAccessibleName * since: v1.44 * langs: diff --git a/docs/src/api/params.md b/docs/src/api/params.md index e059fffe46..a1f909a4fb 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -1003,7 +1003,7 @@ Additional arguments to pass to the browser instance. The list of Chromium flags Browser distribution channel. -Use "chromium" to [opt in to new headless mode](../browsers.md#opt-in-to-new-headless-mode). +Use "chromium" to [opt in to new headless mode](../browsers.md#chromium-new-headless-mode). Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge). diff --git a/docs/src/browsers.md b/docs/src/browsers.md index 90c0b2850b..1cc10d7a8d 100644 --- a/docs/src/browsers.md +++ b/docs/src/browsers.md @@ -338,11 +338,11 @@ dotnet test --settings:webkit.runsettings For Google Chrome, Microsoft Edge and other Chromium-based browsers, by default, Playwright uses open source Chromium builds. Since the Chromium project is ahead of the branded browsers, when the world is on Google Chrome N, Playwright already supports Chromium N+1 that will be released in Google Chrome and Microsoft Edge a few weeks later. -Playwright ships a regular Chromium build for headed operations and a separate [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details. +### Chromium: headless shell -#### Optimize download size on CI +Playwright ships a regular Chromium build for headed operations and a separate [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode. -If you are only running tests in headless shell (i.e. the `channel` option is not specified), for example on CI, you can avoid downloading the full Chromium browser by passing `--only-shell` during installation. +If you are only running tests in headless shell (i.e. the `channel` option is **not** specified), for example on CI, you can avoid downloading the full Chromium browser by passing `--only-shell` during installation. ```bash js # only running tests headlessly @@ -364,7 +364,7 @@ playwright install --with-deps --only-shell pwsh bin/Debug/netX/playwright.ps1 install --with-deps --only-shell ``` -#### Opt-in to new headless mode +### Chromium: new headless mode You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell): @@ -419,6 +419,28 @@ pytest test_login.py --browser-channel chromium dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=chromium ``` +With the new headless mode, you can skip downloading the headless shell during browser installation by using the `--no-shell` option: + +```bash js +# only running tests headlessly +npx playwright install --with-deps --no-shell +``` + +```bash java +# only running tests headlessly +mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps --no-shell" +``` + +```bash python +# only running tests headlessly +playwright install --with-deps --no-shell +``` + +```bash csharp +# only running tests headlessly +pwsh bin/Debug/netX/playwright.ps1 install --with-deps --no-shell +``` + ### Google Chrome & Microsoft Edge While Playwright can download and use the recent Chromium build, it can operate against the branded Google Chrome and Microsoft Edge browsers available on the machine (note that Playwright doesn't install them by default). In particular, the current Playwright version will support Stable and Beta channels of these browsers. diff --git a/docs/src/chrome-extensions-js-python.md b/docs/src/chrome-extensions-js-python.md index 1142e9b3a7..edbe7c06c9 100644 --- a/docs/src/chrome-extensions-js-python.md +++ b/docs/src/chrome-extensions-js-python.md @@ -214,7 +214,7 @@ def test_popup_page(page: Page, extension_id: str) -> None: ## Headless mode -By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using [channel `chromium`](./browsers.md#opt-in-to-new-headless-mode): +By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using [channel `chromium`](./browsers.md#chromium-new-headless-mode): ```js title="fixtures.ts" // ... diff --git a/docs/src/test-webserver-js.md b/docs/src/test-webserver-js.md index aba072d56a..bf01a5cd27 100644 --- a/docs/src/test-webserver-js.md +++ b/docs/src/test-webserver-js.md @@ -36,7 +36,7 @@ export default defineConfig({ | `cwd` | Current working directory of the spawned process, defaults to the directory of the configuration file. | | `stdout` | If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. | | `stderr` | Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. | -| `timeout` | `How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. | +| `timeout` | How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. | ## Adding a server timeout diff --git a/packages/html-reporter/bundle.ts b/packages/html-reporter/bundle.ts index 4c6bc02632..63530cfaad 100644 --- a/packages/html-reporter/bundle.ts +++ b/packages/html-reporter/bundle.ts @@ -51,4 +51,4 @@ export function bundle(): Plugin { } }, }; -} \ No newline at end of file +} diff --git a/packages/html-reporter/src/utils.ts b/packages/html-reporter/src/utils.ts index 65404b2fe7..eec765969b 100644 --- a/packages/html-reporter/src/utils.ts +++ b/packages/html-reporter/src/utils.ts @@ -47,4 +47,3 @@ export function hashStringToInt(str: string) { hash = str.charCodeAt(i) + ((hash << 8) - hash); return Math.abs(hash % 6); } - diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index a163c7a607..3462c09224 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,15 +3,15 @@ "browsers": [ { "name": "chromium", - "revision": "1152", + "revision": "1153", "installByDefault": true, - "browserVersion": "132.0.6834.46" + "browserVersion": "132.0.6834.57" }, { "name": "chromium-tip-of-tree", - "revision": "1288", + "revision": "1290", "installByDefault": false, - "browserVersion": "133.0.6905.0" + "browserVersion": "133.0.6919.0" }, { "name": "firefox", diff --git a/packages/playwright-core/src/common/types.ts b/packages/playwright-core/src/common/types.ts index 55145b5565..f71d4237cd 100644 --- a/packages/playwright-core/src/common/types.ts +++ b/packages/playwright-core/src/common/types.ts @@ -20,4 +20,4 @@ export type Rect = Size & Point; export type Quad = [ Point, Point, Point, Point ]; export type TimeoutOptions = { timeout?: number }; export type NameValue = { name: string, value: string }; -export type HeadersArray = NameValue[]; \ No newline at end of file +export type HeadersArray = NameValue[]; diff --git a/packages/playwright-core/src/image_tools/stats.ts b/packages/playwright-core/src/image_tools/stats.ts index b35371b4a1..79b170243c 100644 --- a/packages/playwright-core/src/image_tools/stats.ts +++ b/packages/playwright-core/src/image_tools/stats.ts @@ -124,4 +124,3 @@ export class FastStats implements Stats { return (this._sum(this._partialSumMult, x1, y1, x2, y2) - this._sum(this._partialSumC1, x1, y1, x2, y2) * this._sum(this._partialSumC2, x1, y1, x2, y2) / N) / N; } } - diff --git a/packages/playwright-core/src/protocol/debug.ts b/packages/playwright-core/src/protocol/debug.ts index 4f44f5941e..f29ae6be82 100644 --- a/packages/playwright-core/src/protocol/debug.ts +++ b/packages/playwright-core/src/protocol/debug.ts @@ -187,4 +187,4 @@ export const pausesBeforeInputActions = new Set([ 'ElementHandle.tap', 'ElementHandle.type', 'ElementHandle.uncheck' -]); \ No newline at end of file +]); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index f4db833e02..9b14551fb8 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2752,4 +2752,4 @@ scheme.JsonPipeSendParams = tObject({ }); scheme.JsonPipeSendResult = tOptional(tObject({})); scheme.JsonPipeCloseParams = tOptional(tObject({})); -scheme.JsonPipeCloseResult = tOptional(tObject({})); \ No newline at end of file +scheme.JsonPipeCloseResult = tOptional(tObject({})); diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 1af083916c..b6303935b7 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -524,5 +524,3 @@ class ClankBrowserProcess implements BrowserProcess { await this._browser.close(); } } - - diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 49d9edde41..d2777c80e9 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -1098,7 +1098,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1109,7 +1109,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1120,7 +1120,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1131,7 +1131,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1142,7 +1142,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1153,7 +1153,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1164,7 +1164,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1175,7 +1175,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1186,7 +1186,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1197,7 +1197,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1208,7 +1208,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1219,7 +1219,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1230,7 +1230,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1241,7 +1241,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1252,7 +1252,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1263,7 +1263,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1274,7 +1274,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1285,7 +1285,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1296,7 +1296,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1307,7 +1307,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1362,7 +1362,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1373,7 +1373,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1384,7 +1384,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1395,7 +1395,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1406,7 +1406,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1417,7 +1417,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1428,7 +1428,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1439,7 +1439,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1450,7 +1450,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1465,7 +1465,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1480,7 +1480,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1495,7 +1495,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1510,7 +1510,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1525,7 +1525,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1540,7 +1540,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1551,7 +1551,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1562,7 +1562,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1577,7 +1577,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36 Edg/132.0.6834.46", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57", "screen": { "width": 1792, "height": 1120 @@ -1622,7 +1622,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1637,7 +1637,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36 Edg/132.0.6834.46", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/src/server/fileUploadUtils.ts b/packages/playwright-core/src/server/fileUploadUtils.ts index 22ac13b127..2696ba0116 100644 --- a/packages/playwright-core/src/server/fileUploadUtils.ts +++ b/packages/playwright-core/src/server/fileUploadUtils.ts @@ -77,4 +77,4 @@ export async function prepareFilesForUpload(frame: Frame, params: channels.Eleme })); return { localPaths, localDirectory, filePayloads }; -} \ No newline at end of file +} diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 92998a7946..1d1c60e6ce 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -436,4 +436,3 @@ function toJugglerProxyOptions(proxy: types.ProxySettings) { // Prefs for quick fixes that didn't make it to the build. // Should all be moved to `playwright.cfg`. const kBandaidFirefoxUserPrefs = {}; - diff --git a/packages/playwright-core/src/server/firefox/firefox.ts b/packages/playwright-core/src/server/firefox/firefox.ts index 9fbc409a56..1e0e4cc055 100644 --- a/packages/playwright-core/src/server/firefox/firefox.ts +++ b/packages/playwright-core/src/server/firefox/firefox.ts @@ -101,4 +101,3 @@ class JugglerReadyState extends BrowserReadyState { this._wsEndpoint.resolve(undefined); } } - diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index a1ffdf893c..21fcca69a4 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -29,7 +29,7 @@ import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser'; import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator'; import type * as channels from '@protocol/channels'; import { Highlight } from './highlight'; -import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription, getReadonly } from './roleUtils'; +import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription, getReadonly, getElementAccessibleErrorMessage } from './roleUtils'; import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils'; import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators'; @@ -1321,6 +1321,8 @@ export class InjectedScript { received = getElementAccessibleName(element, false /* includeHidden */); } else if (expression === 'to.have.accessible.description') { received = getElementAccessibleDescription(element, false /* includeHidden */); + } else if (expression === 'to.have.accessible.error.message') { + received = getElementAccessibleErrorMessage(element); } else if (expression === 'to.have.role') { received = getAriaRole(element) || ''; } else if (expression === 'to.have.title') { diff --git a/packages/playwright-core/src/server/injected/recorder/clipPaths.ts b/packages/playwright-core/src/server/injected/recorder/clipPaths.ts index a1e016542a..1ac490843d 100644 --- a/packages/playwright-core/src/server/injected/recorder/clipPaths.ts +++ b/packages/playwright-core/src/server/injected/recorder/clipPaths.ts @@ -28,4 +28,4 @@ import type { SvgJson } from './recorder'; // eslint-disable-next-line key-spacing, object-curly-spacing, comma-spacing, quotes const svgJson: SvgJson = {"tagName":"svg","children":[{"tagName":"defs","children":[{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gripper"},"children":[{"tagName":"path","attrs":{"d":"M5 3h2v2H5zm0 4h2v2H5zm0 4h2v2H5zm4-8h2v2H9zm0 4h2v2H9zm0 4h2v2H9z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-circle-large-filled"},"children":[{"tagName":"path","attrs":{"d":"M8 1a6.8 6.8 0 0 1 1.86.253 6.899 6.899 0 0 1 3.083 1.805 6.903 6.903 0 0 1 1.804 3.083C14.916 6.738 15 7.357 15 8s-.084 1.262-.253 1.86a6.9 6.9 0 0 1-.704 1.674 7.157 7.157 0 0 1-2.516 2.509 6.966 6.966 0 0 1-1.668.71A6.984 6.984 0 0 1 8 15a6.984 6.984 0 0 1-1.86-.246 7.098 7.098 0 0 1-1.674-.711 7.3 7.3 0 0 1-1.415-1.094 7.295 7.295 0 0 1-1.094-1.415 7.098 7.098 0 0 1-.71-1.675A6.985 6.985 0 0 1 1 8c0-.643.082-1.262.246-1.86a6.968 6.968 0 0 1 .711-1.667 7.156 7.156 0 0 1 2.509-2.516 6.895 6.895 0 0 1 1.675-.704A6.808 6.808 0 0 1 8 1z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-inspect"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-whole-word"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M0 11H1V13H15V11H16V14H15H1H0V11Z"}},{"tagName":"path","attrs":{"d":"M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z"}},{"tagName":"path","attrs":{"d":"M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-eye"},"children":[{"tagName":"path","attrs":{"d":"M7.99993 6.00316C9.47266 6.00316 10.6666 7.19708 10.6666 8.66981C10.6666 10.1426 9.47266 11.3365 7.99993 11.3365C6.52715 11.3365 5.33324 10.1426 5.33324 8.66981C5.33324 7.19708 6.52715 6.00316 7.99993 6.00316ZM7.99993 7.00315C7.07946 7.00315 6.33324 7.74935 6.33324 8.66981C6.33324 9.59028 7.07946 10.3365 7.99993 10.3365C8.9204 10.3365 9.6666 9.59028 9.6666 8.66981C9.6666 7.74935 8.9204 7.00315 7.99993 7.00315ZM7.99993 3.66675C11.0756 3.66675 13.7307 5.76675 14.4673 8.70968C14.5344 8.97755 14.3716 9.24908 14.1037 9.31615C13.8358 9.38315 13.5643 9.22041 13.4973 8.95248C12.8713 6.45205 10.6141 4.66675 7.99993 4.66675C5.38454 4.66675 3.12664 6.45359 2.50182 8.95555C2.43491 9.22341 2.16348 9.38635 1.89557 9.31948C1.62766 9.25255 1.46471 8.98115 1.53162 8.71321C2.26701 5.76856 4.9229 3.66675 7.99993 3.66675Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-symbol-constant"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M4 6h8v1H4V6zm8 3H4v1h8V9z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-check"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M14.431 3.323l-8.47 10-.79-.036-3.35-4.77.818-.574 2.978 4.24 8.051-9.506.764.646z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-close"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-pass"},"children":[{"tagName":"path","attrs":{"d":"M6.27 10.87h.71l4.56-4.56-.71-.71-4.2 4.21-1.92-1.92L4 8.6l2.27 2.27z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gist"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M10.57 1.14l3.28 3.3.15.36v9.7l-.5.5h-11l-.5-.5v-13l.5-.5h7.72l.35.14zM10 5h3l-3-3v3zM3 2v12h10V6H9.5L9 5.5V2H3zm2.062 7.533l1.817-1.828L6.17 7 4 9.179v.707l2.171 2.174.707-.707-1.816-1.82zM8.8 7.714l.7-.709 2.189 2.175v.709L9.5 12.062l-.705-.709 1.831-1.82L8.8 7.714z"}}]}]}]}; -export default svgJson; \ No newline at end of file +export default svgJson; diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index cc9e6a70d0..f74c893c1b 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -461,6 +461,59 @@ export function getElementAccessibleDescription(element: Element, includeHidden: return accessibleDescription; } +// https://www.w3.org/TR/wai-aria-1.2/#aria-invalid +const kAriaInvalidRoles = ['application', 'checkbox', 'combobox', 'gridcell', 'listbox', 'radiogroup', 'slider', 'spinbutton', 'textbox', 'tree', 'columnheader', 'rowheader', 'searchbox', 'switch', 'treegrid']; + +function getAriaInvalid(element: Element): 'false' | 'true' | 'grammar' | 'spelling' { + const role = getAriaRole(element) || ''; + if (!role || !kAriaInvalidRoles.includes(role)) + return 'false'; + const ariaInvalid = element.getAttribute('aria-invalid'); + if (!ariaInvalid || ariaInvalid.trim() === '' || ariaInvalid.toLocaleLowerCase() === 'false') + return 'false'; + if (ariaInvalid === 'true' || ariaInvalid === 'grammar' || ariaInvalid === 'spelling') + return ariaInvalid; + return 'true'; +} + +function getValidityInvalid(element: Element) { + if ('validity' in element){ + const validity = element.validity as ValidityState | undefined; + return validity?.valid === false; + } + return false; +} + +export function getElementAccessibleErrorMessage(element: Element): string { + // SPEC: https://w3c.github.io/aria/#aria-errormessage + // + // TODO: support https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/validationMessage + const cache = cacheAccessibleErrorMessage; + let accessibleErrorMessage = cacheAccessibleErrorMessage?.get(element); + + if (accessibleErrorMessage === undefined) { + accessibleErrorMessage = ''; + + const isAriaInvalid = getAriaInvalid(element) !== 'false'; + const isValidityInvalid = getValidityInvalid(element); + if (isAriaInvalid || isValidityInvalid) { + const errorMessageId = element.getAttribute('aria-errormessage'); + const errorMessages = getIdRefs(element, errorMessageId); + // Ideally, this should be a separate "embeddedInErrorMessage", but it would follow the exact same rules. + // Relevant vague spec: https://w3c.github.io/core-aam/#ariaErrorMessage. + const parts = errorMessages.map(errorMessage => asFlatString( + getTextAlternativeInternal(errorMessage, { + visitedElements: new Set(), + embeddedInDescribedBy: { element: errorMessage, hidden: isElementHiddenForAria(errorMessage) }, + }) + )); + accessibleErrorMessage = parts.join(' ').trim(); + } + cache?.set(element, accessibleErrorMessage); + } + return accessibleErrorMessage; +} + type AccessibleNameOptions = { visitedElements: Set, includeHidden?: boolean, @@ -972,6 +1025,7 @@ let cacheAccessibleName: Map | undefined; let cacheAccessibleNameHidden: Map | undefined; let cacheAccessibleDescription: Map | undefined; let cacheAccessibleDescriptionHidden: Map | undefined; +let cacheAccessibleErrorMessage: Map | undefined; let cacheIsHidden: Map | undefined; let cachePseudoContentBefore: Map | undefined; let cachePseudoContentAfter: Map | undefined; @@ -983,6 +1037,7 @@ export function beginAriaCaches() { cacheAccessibleNameHidden ??= new Map(); cacheAccessibleDescription ??= new Map(); cacheAccessibleDescriptionHidden ??= new Map(); + cacheAccessibleErrorMessage ??= new Map(); cacheIsHidden ??= new Map(); cachePseudoContentBefore ??= new Map(); cachePseudoContentAfter ??= new Map(); @@ -994,6 +1049,7 @@ export function endAriaCaches() { cacheAccessibleNameHidden = undefined; cacheAccessibleDescription = undefined; cacheAccessibleDescriptionHidden = undefined; + cacheAccessibleErrorMessage = undefined; cacheIsHidden = undefined; cachePseudoContentBefore = undefined; cachePseudoContentAfter = undefined; diff --git a/packages/playwright-core/src/server/registry/nativeDeps.ts b/packages/playwright-core/src/server/registry/nativeDeps.ts index 6e25e3a14f..9653210a45 100644 --- a/packages/playwright-core/src/server/registry/nativeDeps.ts +++ b/packages/playwright-core/src/server/registry/nativeDeps.ts @@ -1104,4 +1104,3 @@ deps['debian12-arm64'] = { ...deps['debian12-x64'].lib2package, }, }; - diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 4e850f4a84..2517ea24ce 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -354,4 +354,4 @@ export function rewriteOpenSSLErrorIfNeeded(error: Error): Error { 'For more details, see https://github.com/openssl/openssl/blob/master/README-PROVIDERS.md#the-legacy-provider', 'You could probably modernize the certificate by following the steps at https://github.com/nodejs/node/issues/40672#issuecomment-1243648223', ].join('\n')); -} \ No newline at end of file +} diff --git a/packages/playwright-core/src/server/socksInterceptor.ts b/packages/playwright-core/src/server/socksInterceptor.ts index 498e8bfe73..6b29636a00 100644 --- a/packages/playwright-core/src/server/socksInterceptor.ts +++ b/packages/playwright-core/src/server/socksInterceptor.ts @@ -83,4 +83,3 @@ export class SocksInterceptor { function tChannelForSocks(names: '*' | string[], arg: any, path: string, context: ValidatorContext) { throw new ValidationError(`${path}: channels are not expected in SocksSupport`); } - diff --git a/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts b/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts index b8af1b9ca3..6d7459c978 100644 --- a/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts +++ b/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts @@ -105,4 +105,4 @@ export class WKProvisionalPage { assert(!frameTree.frame.parentId); this._mainFrameId = frameTree.frame.id; } -} \ No newline at end of file +} diff --git a/packages/playwright-core/src/utils/isomorphic/mimeType.ts b/packages/playwright-core/src/utils/isomorphic/mimeType.ts index 2f8b9d4829..407d935281 100644 --- a/packages/playwright-core/src/utils/isomorphic/mimeType.ts +++ b/packages/playwright-core/src/utils/isomorphic/mimeType.ts @@ -20,4 +20,4 @@ export function isJsonMimeType(mimeType: string) { export function isTextualMimeType(mimeType: string) { return !!mimeType.match(/^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$/); -} \ No newline at end of file +} diff --git a/packages/playwright-core/src/utils/linuxUtils.ts b/packages/playwright-core/src/utils/linuxUtils.ts index d8e227f107..5d98c823a5 100644 --- a/packages/playwright-core/src/utils/linuxUtils.ts +++ b/packages/playwright-core/src/utils/linuxUtils.ts @@ -77,4 +77,3 @@ function parseOSReleaseText(osReleaseText: string): Map { } return fields; } - diff --git a/packages/playwright-core/src/utils/sequence.ts b/packages/playwright-core/src/utils/sequence.ts index 27756fabeb..b063e5c488 100644 --- a/packages/playwright-core/src/utils/sequence.ts +++ b/packages/playwright-core/src/utils/sequence.ts @@ -63,4 +63,4 @@ export function findRepeatedSubsequences(s: string[]): { sequence: string[]; cou } return result; -} \ No newline at end of file +} diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index ba83204e7e..78c1c668c4 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -13853,18 +13853,22 @@ export interface Locator { /** * Creates a locator matching all elements that match one or both of the two locators. * - * Note that when both locators match something, the resulting locator will have multiple matches and violate - * [locator strictness](https://playwright.dev/docs/locators#strictness) guidelines. + * Note that when both locators match something, the resulting locator will have multiple matches, potentially causing + * a [locator strictness](https://playwright.dev/docs/locators#strictness) violation. * * **Usage** * * Consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog * shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly. * + * **NOTE** If both "New email" button and security dialog appear on screen, the "or" locator will match both of them, + * possibly throwing the ["strict mode violation" error](https://playwright.dev/docs/locators#strictness). In this case, you can use + * [locator.first()](https://playwright.dev/docs/api/class-locator#locator-first) to only match one of them. + * * ```js * const newEmail = page.getByRole('button', { name: 'New' }); * const dialog = page.getByText('Confirm security settings'); - * await expect(newEmail.or(dialog)).toBeVisible(); + * await expect(newEmail.or(dialog).first()).toBeVisible(); * if (await dialog.isVisible()) * await page.getByRole('button', { name: 'Dismiss' }).click(); * await newEmail.click(); @@ -14716,7 +14720,7 @@ export interface BrowserType { /** * Browser distribution channel. * - * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode). + * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode). * * Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or * "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). @@ -15215,7 +15219,7 @@ export interface BrowserType { /** * Browser distribution channel. * - * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode). + * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode). * * Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or * "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). @@ -21566,7 +21570,7 @@ export interface LaunchOptions { /** * Browser distribution channel. * - * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode). + * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode). * * Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or * "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). diff --git a/packages/playwright/jsx-runtime.mjs b/packages/playwright/jsx-runtime.mjs index 742708825e..1dfd5835aa 100644 --- a/packages/playwright/jsx-runtime.mjs +++ b/packages/playwright/jsx-runtime.mjs @@ -18,4 +18,4 @@ import jsxRuntime from './jsx-runtime.js'; export const jsx = jsxRuntime.jsx; export const jsxs = jsxRuntime.jsxs; -export const Fragment = jsxRuntime.Fragment; \ No newline at end of file +export const Fragment = jsxRuntime.Fragment; diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 0e8babce10..a86fbb5157 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -298,4 +298,4 @@ const configInternalSymbol = Symbol('configInternalSymbol'); export function getProjectId(project: FullProject): string { return (project as any).__projectId!; -} \ No newline at end of file +} diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 0bd116e7a1..d4c3287d33 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -35,6 +35,7 @@ import { toContainText, toHaveAccessibleDescription, toHaveAccessibleName, + toHaveAccessibleErrorMessage, toHaveAttribute, toHaveClass, toHaveCount, @@ -224,6 +225,7 @@ const customAsyncMatchers = { toContainText, toHaveAccessibleDescription, toHaveAccessibleName, + toHaveAccessibleErrorMessage, toHaveAttribute, toHaveClass, toHaveCount, diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 8a8089e91e..3962c0cae9 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -205,6 +205,18 @@ export function toHaveAccessibleName( } } +export function toHaveAccessibleErrorMessage( + this: ExpectMatcherState, + locator: LocatorEx, + expected: string | RegExp, + options?: { timeout?: number; ignoreCase?: boolean }, +) { + return toMatchText.call(this, 'toHaveAccessibleErrorMessage', locator, 'Locator', async (isNot, timeout) => { + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true }); + return await locator._expect('to.have.accessible.error.message', { expectedText: expectedText, isNot, timeout }); + }, expected, options); +} + export function toHaveAttribute( this: ExpectMatcherState, locator: LocatorEx, diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 410b91ef87..a8659f69bc 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -21,7 +21,7 @@ import path from 'path'; import type { TransformCallback } from 'stream'; import { Transform } from 'stream'; import { codeFrameColumns } from '../transform/babelBundle'; -import type { FullResult, FullConfig, Location, Suite, TestCase as TestCasePublic, TestResult as TestResultPublic, TestStep as TestStepPublic, TestError } from '../../types/testReporter'; +import type * as api from '../../types/testReporter'; import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/utils'; import { colors, formatError, formatResultFailure, stripAnsiEscapes } from './base'; import { resolveReporterOutputPath } from '../util'; @@ -56,8 +56,8 @@ type HtmlReporterOptions = { }; class HtmlReporter implements ReporterV2 { - private config!: FullConfig; - private suite!: Suite; + private config!: api.FullConfig; + private suite!: api.Suite; private _options: HtmlReporterOptions; private _outputFolder!: string; private _attachmentsBaseURL!: string; @@ -65,7 +65,7 @@ class HtmlReporter implements ReporterV2 { private _port: number | undefined; private _host: string | undefined; private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined; - private _topLevelErrors: TestError[] = []; + private _topLevelErrors: api.TestError[] = []; constructor(options: HtmlReporterOptions) { this._options = options; @@ -79,11 +79,11 @@ class HtmlReporter implements ReporterV2 { return false; } - onConfigure(config: FullConfig) { + onConfigure(config: api.FullConfig) { this.config = config; } - onBegin(suite: Suite) { + onBegin(suite: api.Suite) { const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions(); this._outputFolder = outputFolder; this._open = open; @@ -125,11 +125,11 @@ class HtmlReporter implements ReporterV2 { return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath); } - onError(error: TestError): void { + onError(error: api.TestError): void { this._topLevelErrors.push(error); } - async onEnd(result: FullResult) { + async onEnd(result: api.FullResult) { const projectSuites = this.suite.suites; await removeFolders([this._outputFolder]); const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL); @@ -223,14 +223,14 @@ export function startHtmlReportServer(folder: string): HttpServer { } class HtmlBuilder { - private _config: FullConfig; + private _config: api.FullConfig; private _reportFolder: string; private _stepsInFile = new MultiMap(); private _dataZipFile: ZipFile; private _hasTraces = false; private _attachmentsBaseURL: string; - constructor(config: FullConfig, outputDir: string, attachmentsBaseURL: string) { + constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string) { this._config = config; this._reportFolder = outputDir; fs.mkdirSync(this._reportFolder, { recursive: true }); @@ -238,7 +238,7 @@ class HtmlBuilder { this._attachmentsBaseURL = attachmentsBaseURL; } - async build(metadata: Metadata, projectSuites: Suite[], result: FullResult, topLevelErrors: TestError[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { + async build(metadata: Metadata, projectSuites: api.Suite[], result: api.FullResult, topLevelErrors: api.TestError[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { const data = new Map(); for (const projectSuite of projectSuites) { for (const fileSuite of projectSuite.suites) { @@ -378,7 +378,7 @@ class HtmlBuilder { this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName); } - private _processSuite(suite: Suite, projectName: string, path: string[], outTests: TestEntry[]) { + private _processSuite(suite: api.Suite, projectName: string, path: string[], outTests: TestEntry[]) { const newPath = [...path, suite.title]; suite.entries().forEach(e => { if (e.type === 'test') @@ -388,7 +388,7 @@ class HtmlBuilder { }); } - private _createTestEntry(test: TestCasePublic, projectName: string, path: string[]): TestEntry { + private _createTestEntry(test: api.TestCase, projectName: string, path: string[]): TestEntry { const duration = test.results.reduce((a, r) => a + r.duration, 0); const location = this._relativeLocation(test.location)!; path = path.slice(1).filter(path => path.length > 0); @@ -500,7 +500,7 @@ class HtmlBuilder { }).filter(Boolean) as TestAttachment[]; } - private _createTestResult(test: TestCasePublic, result: TestResultPublic): TestResult { + private _createTestResult(test: api.TestCase, result: api.TestResult): TestResult { return { duration: result.duration, startTime: result.startTime.toISOString(), @@ -537,7 +537,7 @@ class HtmlBuilder { return testStep; } - private _relativeLocation(location: Location | undefined): Location | undefined { + private _relativeLocation(location: api.Location | undefined): api.Location | undefined { if (!location) return undefined; const file = toPosixPath(path.relative(this._config.rootDir, location.file)); @@ -615,9 +615,9 @@ function stdioAttachment(chunk: Buffer | string, type: 'stdout' | 'stderr'): Jso }; } -type DedupedStep = { step: TestStepPublic, count: number, duration: number }; +type DedupedStep = { step: api.TestStep, count: number, duration: number }; -function dedupeSteps(steps: TestStepPublic[]) { +function dedupeSteps(steps: api.TestStep[]) { const result: DedupedStep[] = []; let lastResult = undefined; for (const step of steps) { diff --git a/packages/playwright/src/runner/vcs.ts b/packages/playwright/src/runner/vcs.ts index 6f7ed55c9a..da7c4c2cc8 100644 --- a/packages/playwright/src/runner/vcs.ts +++ b/packages/playwright/src/runner/vcs.ts @@ -54,4 +54,4 @@ export async function detectChangedTestFiles(baseCommit: string, configDir: stri const trackedFilesWithChanges = gitFileList(`diff ${baseCommit} --name-only`).map(file => path.join(gitRoot, file)); return new Set(affectedTestFiles([...untrackedFiles, ...trackedFilesWithChanges])); -} \ No newline at end of file +} diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index caed95b8d5..7c9cae5415 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -6002,7 +6002,7 @@ export interface PlaywrightWorkerOptions { /** * Browser distribution channel. * - * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode). + * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode). * * Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or * "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). @@ -8112,6 +8112,34 @@ interface LocatorAssertions { timeout?: number; }): Promise; + /** + * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with a given + * [aria errormessage](https://w3c.github.io/aria/#aria-errormessage). + * + * **Usage** + * + * ```js + * const locator = page.getByTestId('username-input'); + * await expect(locator).toHaveAccessibleErrorMessage('Username is required.'); + * ``` + * + * @param errorMessage Expected accessible error message. + * @param options + */ + toHaveAccessibleErrorMessage(errorMessage: string|RegExp, options?: { + /** + * Whether to perform case-insensitive match. + * [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-error-message-option-ignore-case) + * option takes precedence over the corresponding regular expression flag if specified. + */ + ignoreCase?: boolean; + + /** + * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + /** * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with a given * [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 100baf59f9..6f9e36f0c3 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -4983,3 +4983,4 @@ export interface JsonPipeEvents { 'message': JsonPipeMessageEvent; 'closed': JsonPipeClosedEvent; } + diff --git a/packages/trace-viewer/bundle.ts b/packages/trace-viewer/bundle.ts index eaf09a6cc3..38375f8d76 100644 --- a/packages/trace-viewer/bundle.ts +++ b/packages/trace-viewer/bundle.ts @@ -28,4 +28,4 @@ export function bundle(): Plugin { }, }, }; -} \ No newline at end of file +} diff --git a/packages/trace-viewer/src/sw/traceModelBackends.ts b/packages/trace-viewer/src/sw/traceModelBackends.ts index ee694b2fba..4f8ea94c3d 100644 --- a/packages/trace-viewer/src/sw/traceModelBackends.ts +++ b/packages/trace-viewer/src/sw/traceModelBackends.ts @@ -160,4 +160,4 @@ export class TraceViewerServer { return; return response; } -} \ No newline at end of file +} diff --git a/packages/trace-viewer/src/third_party/devtools.ts b/packages/trace-viewer/src/third_party/devtools.ts index 27c520cbce..cc03330240 100644 --- a/packages/trace-viewer/src/third_party/devtools.ts +++ b/packages/trace-viewer/src/third_party/devtools.ts @@ -282,4 +282,4 @@ export async function generateFetchCall(resource: Entry, style: FetchStyle = Fet async function fetchRequestPostData(resource: Entry) { return resource.request.postData?._sha1 ? await fetch(`sha1/${resource.request.postData._sha1}`).then(r => r.text()) : resource.request.postData?.text; -} \ No newline at end of file +} diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index 101c532aea..1deb8ecd88 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -150,4 +150,4 @@ function excludeOrigin(url: string): string { } catch (error) { return url; } -} \ No newline at end of file +} diff --git a/packages/trace-viewer/src/ui/shared/dialog.tsx b/packages/trace-viewer/src/ui/shared/dialog.tsx new file mode 100644 index 0000000000..a58119eca7 --- /dev/null +++ b/packages/trace-viewer/src/ui/shared/dialog.tsx @@ -0,0 +1,145 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import * as React from 'react'; + +export interface DialogProps { + className?: string; + open: boolean; + width: number; + verticalOffset?: number; + requestClose?: () => void; + anchor?: React.RefObject; +} + +export const Dialog: React.FC> = ({ + className, + open, + width, + verticalOffset, + requestClose, + anchor, + children, +}) => { + const dialogRef = React.useRef(null); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, setRecalculateDimensionsCount] = React.useState(0); + + let style: React.CSSProperties | undefined = undefined; + + if (anchor?.current) { + const bounds = anchor.current.getBoundingClientRect(); + + style = { + margin: 0, + top: bounds.bottom + (verticalOffset ?? 0), + left: buildTopLeftCoord(bounds, width), + width, + zIndex: 1, + }; + } + + React.useEffect(() => { + const onClick = (event: MouseEvent) => { + if (!dialogRef.current || !(event.target instanceof Node)) + return; + + if (!dialogRef.current.contains(event.target)) + requestClose?.(); + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') + requestClose?.(); + }; + + if (open) { + document.addEventListener('mousedown', onClick); + document.addEventListener('keydown', onKeyDown); + + return () => { + document.removeEventListener('mousedown', onClick); + document.removeEventListener('keydown', onKeyDown); + }; + } + + return () => {}; + }, [open, requestClose]); + + React.useEffect(() => { + const onResize = () => setRecalculateDimensionsCount(count => count + 1); + + window.addEventListener('resize', onResize); + + return () => { + window.removeEventListener('resize', onResize); + }; + }, []); + + return ( + open && ( + + {children} + + ) + ); +}; + +const buildTopLeftCoord = (bounds: DOMRect, width: number): number => { + const leftAlignCoord = buildTopLeftCoordWithAlignment(bounds, width, 'left'); + + if (leftAlignCoord.inBounds) + return leftAlignCoord.value; + + const rightAlignCoord = buildTopLeftCoordWithAlignment( + bounds, + width, + 'right' + ); + + if (rightAlignCoord.inBounds) + return rightAlignCoord.value; + + return leftAlignCoord.value; +}; + +const buildTopLeftCoordWithAlignment = ( + bounds: DOMRect, + width: number, + alignment: 'left' | 'right' +): { + value: number; + inBounds: boolean; +} => { + const maxLeft = document.documentElement.clientWidth; + + if (alignment === 'left') { + const value = bounds.left; + + return { + value, + inBounds: value + width <= maxLeft, + }; + } else { + const value = bounds.right - width; + + return { + value, + inBounds: bounds.right - width >= 0, + }; + } +}; diff --git a/packages/web/src/components/sourceChooser.tsx b/packages/web/src/components/sourceChooser.tsx index 22b91c61a5..091dce8f98 100644 --- a/packages/web/src/components/sourceChooser.tsx +++ b/packages/web/src/components/sourceChooser.tsx @@ -59,4 +59,4 @@ export function emptySource(): Source { label: '', highlight: [] }; -} \ No newline at end of file +} diff --git a/packages/web/src/components/splitView.spec.tsx b/packages/web/src/components/splitView.spec.tsx index a9260a2d48..ca11520912 100644 --- a/packages/web/src/components/splitView.spec.tsx +++ b/packages/web/src/components/splitView.spec.tsx @@ -90,4 +90,3 @@ test('drag resize', async ({ page, mount }) => { expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 100 }); expect.soft(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 }); }); - diff --git a/tests/android/webview.spec.ts b/tests/android/webview.spec.ts index a0d03b69b6..fac61b5a9a 100644 --- a/tests/android/webview.spec.ts +++ b/tests/android/webview.spec.ts @@ -68,4 +68,4 @@ test('select webview from socketName', async function({ androidDevice }) { await newPage.close(); await context.close(); -}); \ No newline at end of file +}); diff --git a/tests/bidi/csvReporter.ts b/tests/bidi/csvReporter.ts index 8fb936dd11..4986a736c3 100644 --- a/tests/bidi/csvReporter.ts +++ b/tests/bidi/csvReporter.ts @@ -79,4 +79,4 @@ function csvEscape(str) { return str; } -export default CsvReporter; \ No newline at end of file +export default CsvReporter; diff --git a/tests/bidi/expectationReporter.ts b/tests/bidi/expectationReporter.ts index e136149cc4..65371165ff 100644 --- a/tests/bidi/expectationReporter.ts +++ b/tests/bidi/expectationReporter.ts @@ -86,4 +86,4 @@ function getOutcome(test: TestCase): TestExpectation { return 'unknown'; } -export default ExpectationReporter; \ No newline at end of file +export default ExpectationReporter; diff --git a/tests/expect/toThrowMatchers.test.ts b/tests/expect/toThrowMatchers.test.ts index a5d5e52ba8..bc64d13240 100644 --- a/tests/expect/toThrowMatchers.test.ts +++ b/tests/expect/toThrowMatchers.test.ts @@ -588,4 +588,4 @@ for (const toThrow of ['toThrowError', 'toThrow'] as const) { ).toThrowErrorMatchingSnapshot(); }); }); -} \ No newline at end of file +} diff --git a/tests/library/browsercontext-har.spec.ts b/tests/library/browsercontext-har.spec.ts index 4a7834013a..796a37426a 100644 --- a/tests/library/browsercontext-har.spec.ts +++ b/tests/library/browsercontext-har.spec.ts @@ -556,4 +556,4 @@ it('should ignore aborted requests', async ({ contextFactory, server }) => { const result = await Promise.race([evalPromise, page2.waitForTimeout(1000).then(() => 'timeout')]); expect(result).toBe('timeout'); } -}); \ No newline at end of file +}); diff --git a/tests/library/chromium/chromium.spec.ts b/tests/library/chromium/chromium.spec.ts index 61aa811dca..910e0830ff 100644 --- a/tests/library/chromium/chromium.spec.ts +++ b/tests/library/chromium/chromium.spec.ts @@ -629,4 +629,4 @@ test.describe('PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1', () => { const req = await requestPromise; expect(req.headers['x-custom-header']).toBe('custom!'); }); -}); \ No newline at end of file +}); diff --git a/tests/library/inspector/cli-codegen-test.spec.ts b/tests/library/inspector/cli-codegen-test.spec.ts index ad68e75c48..b24871877f 100644 --- a/tests/library/inspector/cli-codegen-test.spec.ts +++ b/tests/library/inspector/cli-codegen-test.spec.ts @@ -122,4 +122,4 @@ test('should generate routeFromHAR with --save-har and --save-har-glob', async ( await cli.waitForCleanExit(); const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8')); expect(json.log.creator.name).toBe('Playwright'); -}); \ No newline at end of file +}); diff --git a/tests/library/popup.spec.ts b/tests/library/popup.spec.ts index 1dcede604b..8f2f1df7dc 100644 --- a/tests/library/popup.spec.ts +++ b/tests/library/popup.spec.ts @@ -288,4 +288,4 @@ async function waitForRafs(page: Page, count: number): Promise { }; window.builtinRequestAnimationFrame(onRaf); }), count); -} \ No newline at end of file +} diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index 0ab5707a62..a1fb6637b1 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -491,6 +491,136 @@ test('toHaveAccessibleDescription', async ({ page }) => { await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz'); }); +test('toHaveAccessibleErrorMessage', async ({ page }) => { + await page.setContent(` +
+ +
Hello
+
This should not be considered.
+
+ `); + + const locator = page.locator('input[role="textbox"]'); + await expect(locator).toHaveAccessibleErrorMessage('Hello'); + await expect(locator).not.toHaveAccessibleErrorMessage('hello'); + await expect(locator).toHaveAccessibleErrorMessage('hello', { ignoreCase: true }); + await expect(locator).toHaveAccessibleErrorMessage(/ell\w/); + await expect(locator).not.toHaveAccessibleErrorMessage(/hello/); + await expect(locator).toHaveAccessibleErrorMessage(/hello/, { ignoreCase: true }); + await expect(locator).not.toHaveAccessibleErrorMessage('This should not be considered.'); +}); + +test('toHaveAccessibleErrorMessage should handle multiple aria-errormessage references', async ({ page }) => { + await page.setContent(` +
+ +
First error message.
+
Second error message.
+
This should not be considered.
+
+ `); + + const locator = page.locator('input[role="textbox"]'); + + await expect(locator).toHaveAccessibleErrorMessage('First error message. Second error message.'); + await expect(locator).toHaveAccessibleErrorMessage(/first error message./i); + await expect(locator).toHaveAccessibleErrorMessage(/second error message./i); + await expect(locator).not.toHaveAccessibleErrorMessage(/This should not be considered./i); +}); + +test.describe('toHaveAccessibleErrorMessage should handle aria-invalid attribute', () => { + const errorMessageText = 'Error message'; + + async function setupPage(page, ariaInvalidValue: string | null) { + const ariaInvalidAttr = ariaInvalidValue === null ? '' : `aria-invalid="${ariaInvalidValue}"`; + await page.setContent(` +
+ +
${errorMessageText}
+
+ `); + return page.locator('#node'); + } + + test.describe('evaluated in false', () => { + test('no aria-invalid attribute', async ({ page }) => { + const locator = await setupPage(page, null); + await expect(locator).not.toHaveAccessibleErrorMessage(errorMessageText); + }); + test('aria-invalid="false"', async ({ page }) => { + const locator = await setupPage(page, 'false'); + await expect(locator).not.toHaveAccessibleErrorMessage(errorMessageText); + }); + test('aria-invalid="" (empty string)', async ({ page }) => { + const locator = await setupPage(page, ''); + await expect(locator).not.toHaveAccessibleErrorMessage(errorMessageText); + }); + }); + test.describe('evaluated in true', () => { + test('aria-invalid="true"', async ({ page }) => { + const locator = await setupPage(page, 'true'); + await expect(locator).toHaveAccessibleErrorMessage(errorMessageText); + }); + test('aria-invalid="foo" (unrecognized value)', async ({ page }) => { + const locator = await setupPage(page, 'foo'); + await expect(locator).toHaveAccessibleErrorMessage(errorMessageText); + }); + }); +}); + +test.describe('toHaveAccessibleErrorMessage should handle validity state with aria-invalid', () => { + const errorMessageText = 'Error message'; + + test('should show error message when validity is false and aria-invalid is true', async ({ page }) => { + await page.setContent(` +
+ +
${errorMessageText}
+
+ `); + const locator = page.locator('#node'); + await locator.fill('101'); + await expect(locator).toHaveAccessibleErrorMessage(errorMessageText); + }); + + test('should show error message when validity is true and aria-invalid is true', async ({ page }) => { + await page.setContent(` +
+ +
${errorMessageText}
+
+ `); + const locator = page.locator('#node'); + await locator.fill('99'); + await expect(locator).toHaveAccessibleErrorMessage(errorMessageText); + }); + + test('should show error message when validity is false and aria-invalid is false', async ({ page }) => { + await page.setContent(` +
+ +
${errorMessageText}
+
+ `); + const locator = page.locator('#node'); + await locator.fill('101'); + await expect(locator).toHaveAccessibleErrorMessage(errorMessageText); + }); + + test('should not show error message when validity is true and aria-invalid is false', async ({ page }) => { + await page.setContent(` +
+ +
${errorMessageText}
+
+ `); + const locator = page.locator('#node'); + await locator.fill('99'); + await expect(locator).not.toHaveAccessibleErrorMessage(errorMessageText); + }); +}); + + test('toHaveRole', async ({ page }) => { await page.setContent(`
Button!
`); await expect(page.locator('div')).toHaveRole('button'); diff --git a/tests/page/frame-evaluate.spec.ts b/tests/page/frame-evaluate.spec.ts index 16bcf6eac7..6908f03e86 100644 --- a/tests/page/frame-evaluate.spec.ts +++ b/tests/page/frame-evaluate.spec.ts @@ -180,4 +180,3 @@ it('evaluateHandle should work', async ({ page, server }) => { const windowHandle = await mainFrame.evaluateHandle(() => window); expect(windowHandle).toBeTruthy(); }); - diff --git a/tests/page/locator-misc-2.spec.ts b/tests/page/locator-misc-2.spec.ts index b32913faba..202b9a2957 100644 --- a/tests/page/locator-misc-2.spec.ts +++ b/tests/page/locator-misc-2.spec.ts @@ -173,4 +173,3 @@ it('Locator.locator() and FrameLocator.locator() should accept locator', async ( expect(await divLocator.locator('input').inputValue()).toBe('outer'); expect(await page.frameLocator('iframe').locator(divLocator).locator('input').inputValue()).toBe('inner'); }); - diff --git a/tests/page/page-event-request.spec.ts b/tests/page/page-event-request.spec.ts index 2c1d7a7eba..e1fc29eb59 100644 --- a/tests/page/page-event-request.spec.ts +++ b/tests/page/page-event-request.spec.ts @@ -272,4 +272,4 @@ it(' resource should have type image', async ({ page }) => { `) ]); expect(request.resourceType()).toBe('image'); -}); \ No newline at end of file +}); diff --git a/tests/page/page-mouse.spec.ts b/tests/page/page-mouse.spec.ts index f93ca4e2e2..7f4ce8a85d 100644 --- a/tests/page/page-mouse.spec.ts +++ b/tests/page/page-mouse.spec.ts @@ -309,4 +309,3 @@ it('should dispatch mouse move after context menu was opened', async ({ page, br } } }); - diff --git a/tests/page/page-request-fulfill.spec.ts b/tests/page/page-request-fulfill.spec.ts index 3edcd0ba0a..0d939a0a5c 100644 --- a/tests/page/page-request-fulfill.spec.ts +++ b/tests/page/page-request-fulfill.spec.ts @@ -485,4 +485,3 @@ it('should not go to the network for fulfilled requests body', { expect(body).toBeTruthy(); expect(serverHit).toBe(false); }); - diff --git a/tests/page/selectors-react.spec.ts b/tests/page/selectors-react.spec.ts index ce47f1bb25..c7bfc3f68a 100644 --- a/tests/page/selectors-react.spec.ts +++ b/tests/page/selectors-react.spec.ts @@ -176,4 +176,3 @@ for (const [name, url] of Object.entries(reacts)) { }); }); } - diff --git a/tests/page/selectors-vue.spec.ts b/tests/page/selectors-vue.spec.ts index 175e1246ee..71b60d1cb3 100644 --- a/tests/page/selectors-vue.spec.ts +++ b/tests/page/selectors-vue.spec.ts @@ -168,4 +168,3 @@ for (const [name, url] of Object.entries(vues)) { }); }); } - diff --git a/tests/playwright-test/command-line-filter.spec.ts b/tests/playwright-test/command-line-filter.spec.ts index 8937694b39..e33cb8b318 100644 --- a/tests/playwright-test/command-line-filter.spec.ts +++ b/tests/playwright-test/command-line-filter.spec.ts @@ -195,4 +195,4 @@ test('should focus a single test suite', async ({ runInlineTest }) => { expect(result.skipped).toBe(0); expect(result.report.suites[0].suites[0].suites[0].specs[0].title).toEqual('pass2'); expect(result.report.suites[0].suites[0].suites[0].specs[1].title).toEqual('pass3'); -}); \ No newline at end of file +}); diff --git a/tests/playwright-test/only-changed.spec.ts b/tests/playwright-test/only-changed.spec.ts index 5a9c6ee121..00614598de 100644 --- a/tests/playwright-test/only-changed.spec.ts +++ b/tests/playwright-test/only-changed.spec.ts @@ -427,4 +427,3 @@ test('exits successfully if there are no changes', async ({ runInlineTest, git, expect(result.exitCode).toBe(0); }); - diff --git a/tests/playwright-test/playwright.reuse.browser.spec.ts b/tests/playwright-test/playwright.reuse.browser.spec.ts index bccce95b3e..4340550f64 100644 --- a/tests/playwright-test/playwright.reuse.browser.spec.ts +++ b/tests/playwright-test/playwright.reuse.browser.spec.ts @@ -148,4 +148,4 @@ test('should produce correct test steps', async ({ runInlineTest, runServer }) = 'onStepEnd fixture: context', 'onStepEnd After Hooks' ]); -}); \ No newline at end of file +}); diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 91b32e76a2..5ddb271b70 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -2052,4 +2052,4 @@ test('project filter in report name', async ({ runInlineTest }) => { const reportFiles = await fs.promises.readdir(reportDir); expect(reportFiles.sort()).toEqual(['report-foo-b-r-6d9d49e-1.zip']); } -}); \ No newline at end of file +}); diff --git a/tests/playwright-test/reporter-dot.spec.ts b/tests/playwright-test/reporter-dot.spec.ts index 5afda3f7bd..04a9337ec0 100644 --- a/tests/playwright-test/reporter-dot.spec.ts +++ b/tests/playwright-test/reporter-dot.spec.ts @@ -112,4 +112,4 @@ for (const useIntermediateMergeReport of [false, true] as const) { colors.green('·').repeat(3)); }); }); -} \ No newline at end of file +} diff --git a/tests/playwright-test/reporter-github.spec.ts b/tests/playwright-test/reporter-github.spec.ts index 100feb157f..292c407b9f 100644 --- a/tests/playwright-test/reporter-github.spec.ts +++ b/tests/playwright-test/reporter-github.spec.ts @@ -98,4 +98,4 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(result.exitCode).toBe(1); }); }); -} \ No newline at end of file +} diff --git a/tests/playwright-test/reporter-junit.spec.ts b/tests/playwright-test/reporter-junit.spec.ts index 2b182e00c0..9947484903 100644 --- a/tests/playwright-test/reporter-junit.spec.ts +++ b/tests/playwright-test/reporter-junit.spec.ts @@ -594,4 +594,4 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(time).toBeGreaterThan(1); }); }); -} \ No newline at end of file +} diff --git a/tests/playwright-test/reporter-line.spec.ts b/tests/playwright-test/reporter-line.spec.ts index 22441d567a..7322af433f 100644 --- a/tests/playwright-test/reporter-line.spec.ts +++ b/tests/playwright-test/reporter-line.spec.ts @@ -189,4 +189,4 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(result.exitCode).toBe(1); }); }); -} \ No newline at end of file +} diff --git a/tests/playwright-test/reporter-list.spec.ts b/tests/playwright-test/reporter-list.spec.ts index 752d29c649..1d488cc253 100644 --- a/tests/playwright-test/reporter-list.spec.ts +++ b/tests/playwright-test/reporter-list.spec.ts @@ -319,4 +319,3 @@ function simpleAnsiRenderer(text, ttyWidth) { return screenLines.map(line => line.join('')).join('\n'); } - diff --git a/tests/playwright-test/reporter-markdown.spec.ts b/tests/playwright-test/reporter-markdown.spec.ts index 076e28d66e..558a1f8a84 100644 --- a/tests/playwright-test/reporter-markdown.spec.ts +++ b/tests/playwright-test/reporter-markdown.spec.ts @@ -157,4 +157,4 @@ test('report with worker error', async ({ runInlineTest }) => { **0 passed** :heavy_check_mark::heavy_check_mark::heavy_check_mark: `); -}); \ No newline at end of file +}); diff --git a/tests/playwright-test/snapshot-path-template.spec.ts b/tests/playwright-test/snapshot-path-template.spec.ts index 4f260469b7..2fd79403f4 100644 --- a/tests/playwright-test/snapshot-path-template.spec.ts +++ b/tests/playwright-test/snapshot-path-template.spec.ts @@ -137,4 +137,3 @@ test('arg should receive default arg', async ({ runInlineTest }, testInfo) => { expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`); expect(fs.existsSync(snapshotOutputPath)).toBe(true); }); - diff --git a/tests/playwright-test/test-ignore.spec.ts b/tests/playwright-test/test-ignore.spec.ts index b552380faa..17e3b3adae 100644 --- a/tests/playwright-test/test-ignore.spec.ts +++ b/tests/playwright-test/test-ignore.spec.ts @@ -370,4 +370,4 @@ test('should always work with unix separators', async ({ runInlineTest }) => { expect(result.passed).toBe(1); expect(result.report.suites.map(s => s.file).sort()).toEqual(['a.test.ts']); expect(result.exitCode).toBe(0); -}); \ No newline at end of file +}); diff --git a/tests/playwright-test/test-use.spec.ts b/tests/playwright-test/test-use.spec.ts index 8a2bcd2401..1687cbe2e7 100644 --- a/tests/playwright-test/test-use.spec.ts +++ b/tests/playwright-test/test-use.spec.ts @@ -186,4 +186,3 @@ test('test.use() should throw if called from beforeAll ', async ({ runInlineTest expect(result.exitCode).toBe(1); expect(result.output).toContain('Playwright Test did not expect test.use() to be called here'); }); - diff --git a/tests/playwright-test/ui-mode-test-annotations.spec.ts b/tests/playwright-test/ui-mode-test-annotations.spec.ts index eeff6a5aca..cc1f0b5f04 100644 --- a/tests/playwright-test/ui-mode-test-annotations.spec.ts +++ b/tests/playwright-test/ui-mode-test-annotations.spec.ts @@ -46,4 +46,3 @@ test('should display annotations', async ({ runUITest }) => { await expect(annotations.locator('.annotation-item').filter({ hasText: 'test repo' }).locator('a')) .toHaveAttribute('href', 'https://github.com/microsoft/playwright'); }); - diff --git a/utils/doclint/linting-code-snippets/cli.js b/utils/doclint/linting-code-snippets/cli.js index 5d3200aa9e..7139bb105a 100644 --- a/utils/doclint/linting-code-snippets/cli.js +++ b/utils/doclint/linting-code-snippets/cli.js @@ -153,6 +153,7 @@ class JSLintingService extends LintingService { '@typescript-eslint/no-unused-vars': 'off', 'max-len': ['error', { code: 100 }], 'react/react-in-jsx-scope': 'off', + 'eol-last': 'off', }, } }); diff --git a/utils/generate_channels.js b/utils/generate_channels.js index 0bebcbba0c..827250ad8c 100755 --- a/utils/generate_channels.js +++ b/utils/generate_channels.js @@ -362,7 +362,7 @@ function writeFile(filePath, content) { fs.writeFileSync(filePath, content, 'utf8'); } -writeFile(path.join(__dirname, '..', 'packages', 'protocol', 'src', 'channels.d.ts'), channels_ts.join('\n')); -writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'debug.ts'), debug_ts.join('\n')); -writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'validator.ts'), validator_ts.join('\n')); +writeFile(path.join(__dirname, '..', 'packages', 'protocol', 'src', 'channels.d.ts'), channels_ts.join('\n') + '\n'); +writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'debug.ts'), debug_ts.join('\n') + '\n'); +writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'validator.ts'), validator_ts.join('\n') + '\n'); process.exit(hasChanges ? 1 : 0); diff --git a/utils/generate_clip_paths.js b/utils/generate_clip_paths.js index 83d26a905c..184b71d36a 100644 --- a/utils/generate_clip_paths.js +++ b/utils/generate_clip_paths.js @@ -95,6 +95,7 @@ const iconNames = [ `// eslint-disable-next-line key-spacing, object-curly-spacing, comma-spacing, quotes`, `const svgJson: SvgJson = ${JSON.stringify(svgJson)};`, `export default svgJson;`, + '', ].join('\n'); fs.writeFileSync(outFile, code, 'utf-8'); })();