Merge branch 'main' into teststep-attachments
This commit is contained in:
commit
2adda217f6
|
|
@ -115,7 +115,7 @@ module.exports = {
|
||||||
"@typescript-eslint/type-annotation-spacing": 2,
|
"@typescript-eslint/type-annotation-spacing": 2,
|
||||||
|
|
||||||
// file whitespace
|
// 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-mixed-spaces-and-tabs": 2,
|
||||||
"no-trailing-spaces": 2,
|
"no-trailing-spaces": 2,
|
||||||
"linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ],
|
"linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ],
|
||||||
|
|
@ -123,6 +123,7 @@ module.exports = {
|
||||||
"key-spacing": [2, {
|
"key-spacing": [2, {
|
||||||
"beforeColon": false
|
"beforeColon": false
|
||||||
}],
|
}],
|
||||||
|
"eol-last": 2,
|
||||||
|
|
||||||
// copyright
|
// copyright
|
||||||
"notice/notice": [2, {
|
"notice/notice": [2, {
|
||||||
|
|
|
||||||
19
.github/workflows/tests_bidi.yml
vendored
19
.github/workflows/tests_bidi.yml
vendored
|
|
@ -48,6 +48,23 @@ jobs:
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: csv-report
|
name: csv-report-${{ matrix.channel }}
|
||||||
path: test-results/report.csv
|
path: test-results/report.csv
|
||||||
retention-days: 7
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# 🎭 Playwright
|
# 🎭 Playwright
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||||
|
|
||||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
## [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 |
|
| | Linux | macOS | Windows |
|
||||||
| :--- | :---: | :---: | :---: |
|
| :--- | :---: | :---: | :---: |
|
||||||
| Chromium <!-- GEN:chromium-version -->132.0.6834.46<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Chromium <!-- GEN:chromium-version -->132.0.6834.57<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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**
|
**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.
|
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
|
```js
|
||||||
const newEmail = page.getByRole('button', { name: 'New' });
|
const newEmail = page.getByRole('button', { name: 'New' });
|
||||||
const dialog = page.getByText('Confirm security settings');
|
const dialog = page.getByText('Confirm security settings');
|
||||||
await expect(newEmail.or(dialog)).toBeVisible();
|
await expect(newEmail.or(dialog).first()).toBeVisible();
|
||||||
if (await dialog.isVisible())
|
if (await dialog.isVisible())
|
||||||
await page.getByRole('button', { name: 'Dismiss' }).click();
|
await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||||
await newEmail.click();
|
await newEmail.click();
|
||||||
|
|
@ -1735,7 +1740,7 @@ await newEmail.click();
|
||||||
```java
|
```java
|
||||||
Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New"));
|
Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New"));
|
||||||
Locator dialog = page.getByText("Confirm security settings");
|
Locator dialog = page.getByText("Confirm security settings");
|
||||||
assertThat(newEmail.or(dialog)).isVisible();
|
assertThat(newEmail.or(dialog).first()).isVisible();
|
||||||
if (dialog.isVisible())
|
if (dialog.isVisible())
|
||||||
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click();
|
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click();
|
||||||
newEmail.click();
|
newEmail.click();
|
||||||
|
|
@ -1744,7 +1749,7 @@ newEmail.click();
|
||||||
```python async
|
```python async
|
||||||
new_email = page.get_by_role("button", name="New")
|
new_email = page.get_by_role("button", name="New")
|
||||||
dialog = page.get_by_text("Confirm security settings")
|
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()):
|
if (await dialog.is_visible()):
|
||||||
await page.get_by_role("button", name="Dismiss").click()
|
await page.get_by_role("button", name="Dismiss").click()
|
||||||
await new_email.click()
|
await new_email.click()
|
||||||
|
|
@ -1753,7 +1758,7 @@ await new_email.click()
|
||||||
```python sync
|
```python sync
|
||||||
new_email = page.get_by_role("button", name="New")
|
new_email = page.get_by_role("button", name="New")
|
||||||
dialog = page.get_by_text("Confirm security settings")
|
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()):
|
if (dialog.is_visible()):
|
||||||
page.get_by_role("button", name="Dismiss").click()
|
page.get_by_role("button", name="Dismiss").click()
|
||||||
new_email.click()
|
new_email.click()
|
||||||
|
|
@ -1762,7 +1767,7 @@ new_email.click()
|
||||||
```csharp
|
```csharp
|
||||||
var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" });
|
var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" });
|
||||||
var dialog = page.GetByText("Confirm security settings");
|
var dialog = page.GetByText("Confirm security settings");
|
||||||
await Expect(newEmail.Or(dialog)).ToBeVisibleAsync();
|
await Expect(newEmail.Or(dialog).First).ToBeVisibleAsync();
|
||||||
if (await dialog.IsVisibleAsync())
|
if (await dialog.IsVisibleAsync())
|
||||||
await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).ClickAsync();
|
await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).ClickAsync();
|
||||||
await newEmail.ClickAsync();
|
await newEmail.ClickAsync();
|
||||||
|
|
|
||||||
|
|
@ -1217,6 +1217,56 @@ Expected accessible description.
|
||||||
* since: v1.44
|
* 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
|
## async method: LocatorAssertions.toHaveAccessibleName
|
||||||
* since: v1.44
|
* since: v1.44
|
||||||
* langs:
|
* langs:
|
||||||
|
|
|
||||||
|
|
@ -1003,7 +1003,7 @@ Additional arguments to pass to the browser instance. The list of Chromium flags
|
||||||
|
|
||||||
Browser distribution channel.
|
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).
|
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).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
```bash js
|
||||||
# only running tests headlessly
|
# 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
|
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):
|
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
|
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
|
### 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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ def test_popup_page(page: Page, extension_id: str) -> None:
|
||||||
|
|
||||||
## Headless mode
|
## 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"
|
```js title="fixtures.ts"
|
||||||
// ...
|
// ...
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default defineConfig({
|
||||||
| `cwd` | Current working directory of the spawned process, defaults to the directory of the configuration file. |
|
| `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"`. |
|
| `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"`. |
|
| `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
|
## Adding a server timeout
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,3 @@ export function hashStringToInt(str: string) {
|
||||||
hash = str.charCodeAt(i) + ((hash << 8) - hash);
|
hash = str.charCodeAt(i) + ((hash << 8) - hash);
|
||||||
return Math.abs(hash % 6);
|
return Math.abs(hash % 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@
|
||||||
"browsers": [
|
"browsers": [
|
||||||
{
|
{
|
||||||
"name": "chromium",
|
"name": "chromium",
|
||||||
"revision": "1152",
|
"revision": "1153",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "132.0.6834.46"
|
"browserVersion": "132.0.6834.57"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-tip-of-tree",
|
"name": "chromium-tip-of-tree",
|
||||||
"revision": "1288",
|
"revision": "1290",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "133.0.6905.0"
|
"browserVersion": "133.0.6919.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox",
|
"name": "firefox",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -524,5 +524,3 @@ class ClankBrowserProcess implements BrowserProcess {
|
||||||
await this._browser.close();
|
await this._browser.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Galaxy S5": {
|
"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": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S5 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -132,7 +132,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8": {
|
"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": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 740
|
"height": 740
|
||||||
|
|
@ -143,7 +143,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 740,
|
"width": 740,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+": {
|
"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": {
|
"viewport": {
|
||||||
"width": 320,
|
"width": 320,
|
||||||
"height": 658
|
"height": 658
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+ landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 658,
|
"width": 658,
|
||||||
"height": 320
|
"height": 320
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4": {
|
"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": {
|
"viewport": {
|
||||||
"width": 712,
|
"width": 712,
|
||||||
"height": 1138
|
"height": 1138
|
||||||
|
|
@ -187,7 +187,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 1138,
|
"width": 1138,
|
||||||
"height": 712
|
"height": 712
|
||||||
|
|
@ -1098,7 +1098,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"LG Optimus L70": {
|
"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": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1109,7 +1109,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"LG Optimus L70 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1120,7 +1120,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550": {
|
"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": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1131,7 +1131,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1142,7 +1142,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950": {
|
"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": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1153,7 +1153,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1164,7 +1164,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10": {
|
"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": {
|
"viewport": {
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 1280
|
"height": 1280
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 800
|
"height": 800
|
||||||
|
|
@ -1186,7 +1186,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4": {
|
"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": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1197,7 +1197,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1208,7 +1208,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5": {
|
"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": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1219,7 +1219,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1230,7 +1230,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X": {
|
"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": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1241,7 +1241,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1252,7 +1252,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6": {
|
"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": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1263,7 +1263,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1274,7 +1274,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P": {
|
"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": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1285,7 +1285,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1296,7 +1296,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7": {
|
"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": {
|
"viewport": {
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 960
|
"height": 960
|
||||||
|
|
@ -1307,7 +1307,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 960,
|
"width": 960,
|
||||||
"height": 600
|
"height": 600
|
||||||
|
|
@ -1362,7 +1362,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Pixel 2": {
|
"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": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 731
|
"height": 731
|
||||||
|
|
@ -1373,7 +1373,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 731,
|
"width": 731,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1384,7 +1384,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL": {
|
"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": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 823
|
"height": 823
|
||||||
|
|
@ -1395,7 +1395,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 823,
|
"width": 823,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1406,7 +1406,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3": {
|
"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": {
|
"viewport": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 786
|
"height": 786
|
||||||
|
|
@ -1417,7 +1417,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 786,
|
"width": 786,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1428,7 +1428,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4": {
|
"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": {
|
"viewport": {
|
||||||
"width": 353,
|
"width": 353,
|
||||||
"height": 745
|
"height": 745
|
||||||
|
|
@ -1439,7 +1439,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 745,
|
"width": 745,
|
||||||
"height": 353
|
"height": 353
|
||||||
|
|
@ -1450,7 +1450,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G)": {
|
"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": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 892
|
"height": 892
|
||||||
|
|
@ -1465,7 +1465,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G) landscape": {
|
"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": {
|
"screen": {
|
||||||
"height": 892,
|
"height": 892,
|
||||||
"width": 412
|
"width": 412
|
||||||
|
|
@ -1480,7 +1480,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5": {
|
"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": {
|
"screen": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 851
|
"height": 851
|
||||||
|
|
@ -1495,7 +1495,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5 landscape": {
|
"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": {
|
"screen": {
|
||||||
"width": 851,
|
"width": 851,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1510,7 +1510,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7": {
|
"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": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 915
|
"height": 915
|
||||||
|
|
@ -1525,7 +1525,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7 landscape": {
|
"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": {
|
"screen": {
|
||||||
"width": 915,
|
"width": 915,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1540,7 +1540,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4": {
|
"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": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1551,7 +1551,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4 landscape": {
|
"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": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1562,7 +1562,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Chrome HiDPI": {
|
"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": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1577,7 +1577,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge HiDPI": {
|
"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": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1622,7 +1622,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Desktop Chrome": {
|
"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": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
@ -1637,7 +1637,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge": {
|
"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": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
|
||||||
|
|
@ -436,4 +436,3 @@ function toJugglerProxyOptions(proxy: types.ProxySettings) {
|
||||||
// Prefs for quick fixes that didn't make it to the build.
|
// Prefs for quick fixes that didn't make it to the build.
|
||||||
// Should all be moved to `playwright.cfg`.
|
// Should all be moved to `playwright.cfg`.
|
||||||
const kBandaidFirefoxUserPrefs = {};
|
const kBandaidFirefoxUserPrefs = {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,4 +101,3 @@ class JugglerReadyState extends BrowserReadyState {
|
||||||
this._wsEndpoint.resolve(undefined);
|
this._wsEndpoint.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser';
|
||||||
import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator';
|
import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { Highlight } from './highlight';
|
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 { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
||||||
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
||||||
import type { Language } from '../../utils/isomorphic/locatorGenerators';
|
import type { Language } from '../../utils/isomorphic/locatorGenerators';
|
||||||
|
|
@ -1321,6 +1321,8 @@ export class InjectedScript {
|
||||||
received = getElementAccessibleName(element, false /* includeHidden */);
|
received = getElementAccessibleName(element, false /* includeHidden */);
|
||||||
} else if (expression === 'to.have.accessible.description') {
|
} else if (expression === 'to.have.accessible.description') {
|
||||||
received = getElementAccessibleDescription(element, false /* includeHidden */);
|
received = getElementAccessibleDescription(element, false /* includeHidden */);
|
||||||
|
} else if (expression === 'to.have.accessible.error.message') {
|
||||||
|
received = getElementAccessibleErrorMessage(element);
|
||||||
} else if (expression === 'to.have.role') {
|
} else if (expression === 'to.have.role') {
|
||||||
received = getAriaRole(element) || '';
|
received = getAriaRole(element) || '';
|
||||||
} else if (expression === 'to.have.title') {
|
} else if (expression === 'to.have.title') {
|
||||||
|
|
|
||||||
|
|
@ -461,6 +461,59 @@ export function getElementAccessibleDescription(element: Element, includeHidden:
|
||||||
return accessibleDescription;
|
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 = {
|
type AccessibleNameOptions = {
|
||||||
visitedElements: Set<Element>,
|
visitedElements: Set<Element>,
|
||||||
includeHidden?: boolean,
|
includeHidden?: boolean,
|
||||||
|
|
@ -972,6 +1025,7 @@ let cacheAccessibleName: Map<Element, string> | undefined;
|
||||||
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
|
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
|
||||||
let cacheAccessibleDescription: Map<Element, string> | undefined;
|
let cacheAccessibleDescription: Map<Element, string> | undefined;
|
||||||
let cacheAccessibleDescriptionHidden: Map<Element, string> | undefined;
|
let cacheAccessibleDescriptionHidden: Map<Element, string> | undefined;
|
||||||
|
let cacheAccessibleErrorMessage: Map<Element, string> | undefined;
|
||||||
let cacheIsHidden: Map<Element, boolean> | undefined;
|
let cacheIsHidden: Map<Element, boolean> | undefined;
|
||||||
let cachePseudoContentBefore: Map<Element, string> | undefined;
|
let cachePseudoContentBefore: Map<Element, string> | undefined;
|
||||||
let cachePseudoContentAfter: Map<Element, string> | undefined;
|
let cachePseudoContentAfter: Map<Element, string> | undefined;
|
||||||
|
|
@ -983,6 +1037,7 @@ export function beginAriaCaches() {
|
||||||
cacheAccessibleNameHidden ??= new Map();
|
cacheAccessibleNameHidden ??= new Map();
|
||||||
cacheAccessibleDescription ??= new Map();
|
cacheAccessibleDescription ??= new Map();
|
||||||
cacheAccessibleDescriptionHidden ??= new Map();
|
cacheAccessibleDescriptionHidden ??= new Map();
|
||||||
|
cacheAccessibleErrorMessage ??= new Map();
|
||||||
cacheIsHidden ??= new Map();
|
cacheIsHidden ??= new Map();
|
||||||
cachePseudoContentBefore ??= new Map();
|
cachePseudoContentBefore ??= new Map();
|
||||||
cachePseudoContentAfter ??= new Map();
|
cachePseudoContentAfter ??= new Map();
|
||||||
|
|
@ -994,6 +1049,7 @@ export function endAriaCaches() {
|
||||||
cacheAccessibleNameHidden = undefined;
|
cacheAccessibleNameHidden = undefined;
|
||||||
cacheAccessibleDescription = undefined;
|
cacheAccessibleDescription = undefined;
|
||||||
cacheAccessibleDescriptionHidden = undefined;
|
cacheAccessibleDescriptionHidden = undefined;
|
||||||
|
cacheAccessibleErrorMessage = undefined;
|
||||||
cacheIsHidden = undefined;
|
cacheIsHidden = undefined;
|
||||||
cachePseudoContentBefore = undefined;
|
cachePseudoContentBefore = undefined;
|
||||||
cachePseudoContentAfter = undefined;
|
cachePseudoContentAfter = undefined;
|
||||||
|
|
|
||||||
|
|
@ -1104,4 +1104,3 @@ deps['debian12-arm64'] = {
|
||||||
...deps['debian12-x64'].lib2package,
|
...deps['debian12-x64'].lib2package,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,4 +83,3 @@ export class SocksInterceptor {
|
||||||
function tChannelForSocks(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
|
function tChannelForSocks(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
|
||||||
throw new ValidationError(`${path}: channels are not expected in SocksSupport`);
|
throw new ValidationError(`${path}: channels are not expected in SocksSupport`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,4 +77,3 @@ function parseOSReleaseText(osReleaseText: string): Map<string, string> {
|
||||||
}
|
}
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
16
packages/playwright-core/types/types.d.ts
vendored
16
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -13853,18 +13853,22 @@ export interface Locator {
|
||||||
/**
|
/**
|
||||||
* Creates a locator matching all elements that match one or both of the two locators.
|
* 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
|
* Note that when both locators match something, the resulting locator will have multiple matches, potentially causing
|
||||||
* [locator strictness](https://playwright.dev/docs/locators#strictness) guidelines.
|
* a [locator strictness](https://playwright.dev/docs/locators#strictness) violation.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
* Consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog
|
* 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.
|
* 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
|
* ```js
|
||||||
* const newEmail = page.getByRole('button', { name: 'New' });
|
* const newEmail = page.getByRole('button', { name: 'New' });
|
||||||
* const dialog = page.getByText('Confirm security settings');
|
* const dialog = page.getByText('Confirm security settings');
|
||||||
* await expect(newEmail.or(dialog)).toBeVisible();
|
* await expect(newEmail.or(dialog).first()).toBeVisible();
|
||||||
* if (await dialog.isVisible())
|
* if (await dialog.isVisible())
|
||||||
* await page.getByRole('button', { name: 'Dismiss' }).click();
|
* await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||||
* await newEmail.click();
|
* await newEmail.click();
|
||||||
|
|
@ -14716,7 +14720,7 @@ export interface BrowserType<Unused = {}> {
|
||||||
/**
|
/**
|
||||||
* Browser distribution channel.
|
* 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
|
* 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).
|
* "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<Unused = {}> {
|
||||||
/**
|
/**
|
||||||
* Browser distribution channel.
|
* 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
|
* 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).
|
* "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.
|
* 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
|
* 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).
|
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
toContainText,
|
toContainText,
|
||||||
toHaveAccessibleDescription,
|
toHaveAccessibleDescription,
|
||||||
toHaveAccessibleName,
|
toHaveAccessibleName,
|
||||||
|
toHaveAccessibleErrorMessage,
|
||||||
toHaveAttribute,
|
toHaveAttribute,
|
||||||
toHaveClass,
|
toHaveClass,
|
||||||
toHaveCount,
|
toHaveCount,
|
||||||
|
|
@ -224,6 +225,7 @@ const customAsyncMatchers = {
|
||||||
toContainText,
|
toContainText,
|
||||||
toHaveAccessibleDescription,
|
toHaveAccessibleDescription,
|
||||||
toHaveAccessibleName,
|
toHaveAccessibleName,
|
||||||
|
toHaveAccessibleErrorMessage,
|
||||||
toHaveAttribute,
|
toHaveAttribute,
|
||||||
toHaveClass,
|
toHaveClass,
|
||||||
toHaveCount,
|
toHaveCount,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
export function toHaveAttribute(
|
||||||
this: ExpectMatcherState,
|
this: ExpectMatcherState,
|
||||||
locator: LocatorEx,
|
locator: LocatorEx,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import path from 'path';
|
||||||
import type { TransformCallback } from 'stream';
|
import type { TransformCallback } from 'stream';
|
||||||
import { Transform } from 'stream';
|
import { Transform } from 'stream';
|
||||||
import { codeFrameColumns } from '../transform/babelBundle';
|
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 { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/utils';
|
||||||
import { colors, formatError, formatResultFailure, stripAnsiEscapes } from './base';
|
import { colors, formatError, formatResultFailure, stripAnsiEscapes } from './base';
|
||||||
import { resolveReporterOutputPath } from '../util';
|
import { resolveReporterOutputPath } from '../util';
|
||||||
|
|
@ -56,8 +56,8 @@ type HtmlReporterOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
class HtmlReporter implements ReporterV2 {
|
class HtmlReporter implements ReporterV2 {
|
||||||
private config!: FullConfig;
|
private config!: api.FullConfig;
|
||||||
private suite!: Suite;
|
private suite!: api.Suite;
|
||||||
private _options: HtmlReporterOptions;
|
private _options: HtmlReporterOptions;
|
||||||
private _outputFolder!: string;
|
private _outputFolder!: string;
|
||||||
private _attachmentsBaseURL!: string;
|
private _attachmentsBaseURL!: string;
|
||||||
|
|
@ -65,7 +65,7 @@ class HtmlReporter implements ReporterV2 {
|
||||||
private _port: number | undefined;
|
private _port: number | undefined;
|
||||||
private _host: string | undefined;
|
private _host: string | undefined;
|
||||||
private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined;
|
private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined;
|
||||||
private _topLevelErrors: TestError[] = [];
|
private _topLevelErrors: api.TestError[] = [];
|
||||||
|
|
||||||
constructor(options: HtmlReporterOptions) {
|
constructor(options: HtmlReporterOptions) {
|
||||||
this._options = options;
|
this._options = options;
|
||||||
|
|
@ -79,11 +79,11 @@ class HtmlReporter implements ReporterV2 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfigure(config: FullConfig) {
|
onConfigure(config: api.FullConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
onBegin(suite: Suite) {
|
onBegin(suite: api.Suite) {
|
||||||
const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
|
const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
|
||||||
this._outputFolder = outputFolder;
|
this._outputFolder = outputFolder;
|
||||||
this._open = open;
|
this._open = open;
|
||||||
|
|
@ -125,11 +125,11 @@ class HtmlReporter implements ReporterV2 {
|
||||||
return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
onError(error: TestError): void {
|
onError(error: api.TestError): void {
|
||||||
this._topLevelErrors.push(error);
|
this._topLevelErrors.push(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onEnd(result: FullResult) {
|
async onEnd(result: api.FullResult) {
|
||||||
const projectSuites = this.suite.suites;
|
const projectSuites = this.suite.suites;
|
||||||
await removeFolders([this._outputFolder]);
|
await removeFolders([this._outputFolder]);
|
||||||
const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL);
|
const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL);
|
||||||
|
|
@ -223,14 +223,14 @@ export function startHtmlReportServer(folder: string): HttpServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
class HtmlBuilder {
|
class HtmlBuilder {
|
||||||
private _config: FullConfig;
|
private _config: api.FullConfig;
|
||||||
private _reportFolder: string;
|
private _reportFolder: string;
|
||||||
private _stepsInFile = new MultiMap<string, TestStep>();
|
private _stepsInFile = new MultiMap<string, TestStep>();
|
||||||
private _dataZipFile: ZipFile;
|
private _dataZipFile: ZipFile;
|
||||||
private _hasTraces = false;
|
private _hasTraces = false;
|
||||||
private _attachmentsBaseURL: string;
|
private _attachmentsBaseURL: string;
|
||||||
|
|
||||||
constructor(config: FullConfig, outputDir: string, attachmentsBaseURL: string) {
|
constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string) {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
this._reportFolder = outputDir;
|
this._reportFolder = outputDir;
|
||||||
fs.mkdirSync(this._reportFolder, { recursive: true });
|
fs.mkdirSync(this._reportFolder, { recursive: true });
|
||||||
|
|
@ -238,7 +238,7 @@ class HtmlBuilder {
|
||||||
this._attachmentsBaseURL = attachmentsBaseURL;
|
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<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
|
const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
|
||||||
for (const projectSuite of projectSuites) {
|
for (const projectSuite of projectSuites) {
|
||||||
for (const fileSuite of projectSuite.suites) {
|
for (const fileSuite of projectSuite.suites) {
|
||||||
|
|
@ -378,7 +378,7 @@ class HtmlBuilder {
|
||||||
this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
|
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];
|
const newPath = [...path, suite.title];
|
||||||
suite.entries().forEach(e => {
|
suite.entries().forEach(e => {
|
||||||
if (e.type === 'test')
|
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 duration = test.results.reduce((a, r) => a + r.duration, 0);
|
||||||
const location = this._relativeLocation(test.location)!;
|
const location = this._relativeLocation(test.location)!;
|
||||||
path = path.slice(1).filter(path => path.length > 0);
|
path = path.slice(1).filter(path => path.length > 0);
|
||||||
|
|
@ -500,7 +500,7 @@ class HtmlBuilder {
|
||||||
}).filter(Boolean) as TestAttachment[];
|
}).filter(Boolean) as TestAttachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createTestResult(test: TestCasePublic, result: TestResultPublic): TestResult {
|
private _createTestResult(test: api.TestCase, result: api.TestResult): TestResult {
|
||||||
return {
|
return {
|
||||||
duration: result.duration,
|
duration: result.duration,
|
||||||
startTime: result.startTime.toISOString(),
|
startTime: result.startTime.toISOString(),
|
||||||
|
|
@ -537,7 +537,7 @@ class HtmlBuilder {
|
||||||
return testStep;
|
return testStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _relativeLocation(location: Location | undefined): Location | undefined {
|
private _relativeLocation(location: api.Location | undefined): api.Location | undefined {
|
||||||
if (!location)
|
if (!location)
|
||||||
return undefined;
|
return undefined;
|
||||||
const file = toPosixPath(path.relative(this._config.rootDir, location.file));
|
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[] = [];
|
const result: DedupedStep[] = [];
|
||||||
let lastResult = undefined;
|
let lastResult = undefined;
|
||||||
for (const step of steps) {
|
for (const step of steps) {
|
||||||
|
|
|
||||||
30
packages/playwright/types/test.d.ts
vendored
30
packages/playwright/types/test.d.ts
vendored
|
|
@ -6002,7 +6002,7 @@ export interface PlaywrightWorkerOptions {
|
||||||
/**
|
/**
|
||||||
* Browser distribution channel.
|
* 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
|
* 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).
|
* "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;
|
timeout?: number;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with a given
|
* 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).
|
* [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||||
|
|
|
||||||
1
packages/protocol/src/channels.d.ts
vendored
1
packages/protocol/src/channels.d.ts
vendored
|
|
@ -4983,3 +4983,4 @@ export interface JsonPipeEvents {
|
||||||
'message': JsonPipeMessageEvent;
|
'message': JsonPipeMessageEvent;
|
||||||
'closed': JsonPipeClosedEvent;
|
'closed': JsonPipeClosedEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
145
packages/trace-viewer/src/ui/shared/dialog.tsx
Normal file
145
packages/trace-viewer/src/ui/shared/dialog.tsx
Normal file
|
|
@ -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<HTMLElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
|
||||||
|
className,
|
||||||
|
open,
|
||||||
|
width,
|
||||||
|
verticalOffset,
|
||||||
|
requestClose,
|
||||||
|
anchor,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const dialogRef = React.useRef<HTMLDialogElement>(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 && (
|
||||||
|
<dialog ref={dialogRef} style={style} className={className} open>
|
||||||
|
{children}
|
||||||
|
</dialog>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -90,4 +90,3 @@ test('drag resize', async ({ page, mount }) => {
|
||||||
expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 100 });
|
expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 100 });
|
||||||
expect.soft(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 });
|
expect.soft(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,136 @@ test('toHaveAccessibleDescription', async ({ page }) => {
|
||||||
await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz');
|
await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('toHaveAccessibleErrorMessage', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<form>
|
||||||
|
<input role="textbox" aria-invalid="true" aria-errormessage="error-message" />
|
||||||
|
<div id="error-message">Hello</div>
|
||||||
|
<div id="irrelevant-error">This should not be considered.</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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(`
|
||||||
|
<form>
|
||||||
|
<input role="textbox" aria-invalid="true" aria-errormessage="error1 error2" />
|
||||||
|
<div id="error1">First error message.</div>
|
||||||
|
<div id="error2">Second error message.</div>
|
||||||
|
<div id="irrelevant-error">This should not be considered.</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" ${ariaInvalidAttr} aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
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(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" type="number" min="1" max="100" aria-invalid="true" aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
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(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" type="number" min="1" max="100" aria-invalid="true" aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
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(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" type="number" min="1" max="100" aria-invalid="false" aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
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(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" type="number" min="1" max="100" aria-invalid="false" aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await locator.fill('99');
|
||||||
|
await expect(locator).not.toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test('toHaveRole', async ({ page }) => {
|
test('toHaveRole', async ({ page }) => {
|
||||||
await page.setContent(`<div role="button">Button!</div>`);
|
await page.setContent(`<div role="button">Button!</div>`);
|
||||||
await expect(page.locator('div')).toHaveRole('button');
|
await expect(page.locator('div')).toHaveRole('button');
|
||||||
|
|
|
||||||
|
|
@ -180,4 +180,3 @@ it('evaluateHandle should work', async ({ page, server }) => {
|
||||||
const windowHandle = await mainFrame.evaluateHandle(() => window);
|
const windowHandle = await mainFrame.evaluateHandle(() => window);
|
||||||
expect(windowHandle).toBeTruthy();
|
expect(windowHandle).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,4 +173,3 @@ it('Locator.locator() and FrameLocator.locator() should accept locator', async (
|
||||||
expect(await divLocator.locator('input').inputValue()).toBe('outer');
|
expect(await divLocator.locator('input').inputValue()).toBe('outer');
|
||||||
expect(await page.frameLocator('iframe').locator(divLocator).locator('input').inputValue()).toBe('inner');
|
expect(await page.frameLocator('iframe').locator(divLocator).locator('input').inputValue()).toBe('inner');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -309,4 +309,3 @@ it('should dispatch mouse move after context menu was opened', async ({ page, br
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -485,4 +485,3 @@ it('should not go to the network for fulfilled requests body', {
|
||||||
expect(body).toBeTruthy();
|
expect(body).toBeTruthy();
|
||||||
expect(serverHit).toBe(false);
|
expect(serverHit).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,4 +176,3 @@ for (const [name, url] of Object.entries(reacts)) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,4 +168,3 @@ for (const [name, url] of Object.entries(vues)) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -427,4 +427,3 @@ test('exits successfully if there are no changes', async ({ runInlineTest, git,
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -319,4 +319,3 @@ function simpleAnsiRenderer(text, ttyWidth) {
|
||||||
|
|
||||||
return screenLines.map(line => line.join('')).join('\n');
|
return screenLines.map(line => line.join('')).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`);
|
||||||
expect(fs.existsSync(snapshotOutputPath)).toBe(true);
|
expect(fs.existsSync(snapshotOutputPath)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,4 +186,3 @@ test('test.use() should throw if called from beforeAll ', async ({ runInlineTest
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.output).toContain('Playwright Test did not expect test.use() to be called here');
|
expect(result.output).toContain('Playwright Test did not expect test.use() to be called here');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,3 @@ test('should display annotations', async ({ runUITest }) => {
|
||||||
await expect(annotations.locator('.annotation-item').filter({ hasText: 'test repo' }).locator('a'))
|
await expect(annotations.locator('.annotation-item').filter({ hasText: 'test repo' }).locator('a'))
|
||||||
.toHaveAttribute('href', 'https://github.com/microsoft/playwright');
|
.toHaveAttribute('href', 'https://github.com/microsoft/playwright');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ class JSLintingService extends LintingService {
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
'max-len': ['error', { code: 100 }],
|
'max-len': ['error', { code: 100 }],
|
||||||
'react/react-in-jsx-scope': 'off',
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'eol-last': 'off',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -362,7 +362,7 @@ function writeFile(filePath, content) {
|
||||||
fs.writeFileSync(filePath, content, 'utf8');
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFile(path.join(__dirname, '..', 'packages', 'protocol', 'src', 'channels.d.ts'), channels_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'));
|
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'));
|
writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'validator.ts'), validator_ts.join('\n') + '\n');
|
||||||
process.exit(hasChanges ? 1 : 0);
|
process.exit(hasChanges ? 1 : 0);
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ const iconNames = [
|
||||||
`// eslint-disable-next-line key-spacing, object-curly-spacing, comma-spacing, quotes`,
|
`// eslint-disable-next-line key-spacing, object-curly-spacing, comma-spacing, quotes`,
|
||||||
`const svgJson: SvgJson = ${JSON.stringify(svgJson)};`,
|
`const svgJson: SvgJson = ${JSON.stringify(svgJson)};`,
|
||||||
`export default svgJson;`,
|
`export default svgJson;`,
|
||||||
|
'',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
fs.writeFileSync(outFile, code, 'utf-8');
|
fs.writeFileSync(outFile, code, 'utf-8');
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue