Merge branch 'main' into mockingproxy

This commit is contained in:
Simon Knott 2025-01-27 12:10:41 +01:00
commit 0ae6af59a7
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
81 changed files with 2408 additions and 2519 deletions

View file

@ -7,7 +7,7 @@ on:
- main - main
paths: paths:
- .github/workflows/tests_bidi.yml - .github/workflows/tests_bidi.yml
- packages/playwright-core/src/server/bidi/* - packages/playwright-core/src/server/bidi/**
schedule: schedule:
# Run every day at midnight # Run every day at midnight
- cron: '0 0 * * *' - cron: '0 0 * * *'
@ -18,6 +18,7 @@ env:
jobs: jobs:
test_bidi: test_bidi:
name: BiDi name: BiDi
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions: permissions:

View file

@ -20,6 +20,7 @@ env:
jobs: jobs:
test_components: test_components:
name: ${{ matrix.os }} - Node.js ${{ matrix.node-version }} name: ${{ matrix.os }} - Node.js ${{ matrix.node-version }}
if: github.repository == 'microsoft/playwright'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:

View file

@ -21,6 +21,7 @@ env:
jobs: jobs:
test_stress: test_stress:
name: Stress - ${{ matrix.os }} name: Stress - ${{ matrix.os }}
if: github.repository == 'microsoft/playwright'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -57,6 +58,7 @@ jobs:
test_webview2: test_webview2:
name: WebView2 name: WebView2
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: windows-2022 runs-on: windows-2022
permissions: permissions:
@ -87,6 +89,7 @@ jobs:
test_clock_frozen_time_linux: test_clock_frozen_time_linux:
name: time library - ${{ matrix.clock }} name: time library - ${{ matrix.clock }}
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
permissions: permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed id-token: write # This is required for OIDC login (azure/login) to succeed
@ -112,6 +115,7 @@ jobs:
test_clock_frozen_time_test_runner: test_clock_frozen_time_test_runner:
name: time test runner - ${{ matrix.clock }} name: time test runner - ${{ matrix.clock }}
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
permissions: permissions:
@ -136,6 +140,7 @@ jobs:
test_electron: test_electron:
name: Electron - ${{ matrix.os }} name: Electron - ${{ matrix.os }}
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false

View file

@ -27,6 +27,7 @@ env:
jobs: jobs:
test_linux: test_linux:
name: ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }}) name: ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }})
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -59,6 +60,7 @@ jobs:
test_linux_chromium_tot: test_linux_chromium_tot:
name: ${{ matrix.os }} (chromium tip-of-tree) name: ${{ matrix.os }} (chromium tip-of-tree)
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -83,6 +85,7 @@ jobs:
test_test_runner: test_test_runner:
name: Test Runner name: Test Runner
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -127,6 +130,7 @@ jobs:
test_web_components: test_web_components:
name: Web Components name: Web Components
if: github.repository == 'microsoft/playwright'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -162,6 +166,7 @@ jobs:
test_vscode_extension: test_vscode_extension:
name: VSCode Extension name: VSCode Extension
if: github.repository == 'microsoft/playwright'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
PWTEST_BOT_NAME: "vscode-extension" PWTEST_BOT_NAME: "vscode-extension"
@ -198,6 +203,7 @@ jobs:
test_package_installations: test_package_installations:
name: "Installation Test ${{ matrix.os }}" name: "Installation Test ${{ matrix.os }}"
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false

View file

@ -26,6 +26,7 @@ permissions:
jobs: jobs:
test_linux: test_linux:
name: ${{ matrix.os }} (${{ matrix.browser }}) name: ${{ matrix.os }} (${{ matrix.browser }})
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -46,6 +47,7 @@ jobs:
test_mac: test_mac:
name: ${{ matrix.os }} (${{ matrix.browser }}) name: ${{ matrix.os }} (${{ matrix.browser }})
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -73,6 +75,7 @@ jobs:
test_win: test_win:
name: "Windows" name: "Windows"
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -92,6 +95,7 @@ jobs:
test-package-installations-other-node-versions: test-package-installations-other-node-versions:
name: "Installation Test ${{ matrix.os }} (${{ matrix.node_version }})" name: "Installation Test ${{ matrix.os }} (${{ matrix.node_version }})"
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -125,6 +129,7 @@ jobs:
headed_tests: headed_tests:
name: "headed ${{ matrix.browser }} (${{ matrix.os }})" name: "headed ${{ matrix.browser }} (${{ matrix.os }})"
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -151,6 +156,7 @@ jobs:
transport_linux: transport_linux:
name: "Transport" name: "Transport"
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -172,6 +178,7 @@ jobs:
tracing_linux: tracing_linux:
name: Tracing ${{ matrix.browser }} ${{ matrix.channel }} name: Tracing ${{ matrix.browser }} ${{ matrix.channel }}
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -199,6 +206,7 @@ jobs:
test_chromium_channels: test_chromium_channels:
name: Test ${{ matrix.channel }} on ${{ matrix.runs-on }} name: Test ${{ matrix.channel }} on ${{ matrix.runs-on }}
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
strategy: strategy:
@ -221,6 +229,7 @@ jobs:
chromium_tot: chromium_tot:
name: Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }} name: Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }}
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -243,6 +252,7 @@ jobs:
chromium_tot_headless_shell: chromium_tot_headless_shell:
name: Chromium tip-of-tree headless-shell-${{ matrix.os }} name: Chromium tip-of-tree headless-shell-${{ matrix.os }}
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -264,6 +274,7 @@ jobs:
firefox_beta: firefox_beta:
name: Firefox Beta ${{ matrix.os }} name: Firefox Beta ${{ matrix.os }}
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -285,6 +296,7 @@ jobs:
build-playwright-driver: build-playwright-driver:
name: "build-playwright-driver" name: "build-playwright-driver"
if: github.repository == 'microsoft/playwright'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -298,6 +310,7 @@ jobs:
test_channel_chromium: test_channel_chromium:
name: Test channel=chromium name: Test channel=chromium
if: github.repository == 'microsoft/playwright'
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false

View file

@ -10,6 +10,7 @@ env:
jobs: jobs:
test: test:
name: "Service" name: "Service"
if: github.repository == 'microsoft/playwright'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:

View file

@ -14,6 +14,7 @@ env:
jobs: jobs:
video_linux: video_linux:
name: "Video Linux" name: "Video Linux"
if: github.repository == 'microsoft/playwright'
environment: allow-uploading-flakiness-results environment: allow-uploading-flakiness-results
strategy: strategy:
fail-fast: false fail-fast: false

View file

@ -9,6 +9,7 @@ on:
jobs: jobs:
trigger: trigger:
name: "trigger" name: "trigger"
if: github.repository == 'microsoft/playwright'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- run: | - run: |

2
.gitignore vendored
View file

@ -35,4 +35,4 @@ test-results
.cache/ .cache/
.eslintcache .eslintcache
playwright.env playwright.env
firefox /firefox/

35
FILING_ISSUES.md Normal file
View file

@ -0,0 +1,35 @@
# How to File a Bug Report That Actually Gets Resolved
Make sure youre on the latest Playwright release before filing. Check existing GitHub issues to avoid duplicates.
## Use the Template
Follow the **Bug Report** template. It guides you step-by-step:
- Fill it out thoroughly.
- Clearly list the steps needed to reproduce the bug.
- Provide what you expected to see versus what happened in reality.
- Include system info from `npx envinfo --preset playwright`.
## Keep Your Repro Minimal
We can't parse your entire code base. Reduce it down to the absolute essentials:
- Start a fresh project (`npm init playwright@latest new-project`).
- Add only the code/DOM needed to show the problem.
- Only use major frameworks if necessary (React, Angular, static HTTP server, etc.).
- Avoid adding extra libraries unless absolutely necessary. Note that we won't install any suspect dependencies.
## Why This Matters
- Most issues that lack a repro turn out to be misconfigurations or usage errors.
- We can't fix problems if we cant reproduce them ourselves.
- We cant debug entire private projects or handle sensitive credentials.
- Each confirmed bug will have a test in our repo, so your repro must be as clean as possible.
## More Help
- [Stack Overflows Minimal Reproducible Example Guide](https://stackoverflow.com/help/minimal-reproducible-example)
- [Playwright Debugging Tools](https://playwright.dev/docs/debug)
## Bottom Line
A well-isolated bug speeds up verification and resolution. Minimal, public repro or its unlikely we can assist.

View file

@ -1,6 +1,6 @@
# 🎭 Playwright # 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-133.0.6943.16-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-134.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) [![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-133.0.6943.27-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-134.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) ## [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 -->133.0.6943.16<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Chromium <!-- GEN:chromium-version -->133.0.6943.27<!-- 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 -->134.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox <!-- GEN:firefox-version -->134.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

View file

@ -1,3 +1,3 @@
REMOTE_URL="https://github.com/mozilla/gecko-dev" REMOTE_URL="https://github.com/mozilla/gecko-dev"
BASE_BRANCH="release" BASE_BRANCH="release"
BASE_REVISION="bc78b98043438d8ee2727a483b6e10dedfda883f" BASE_REVISION="5cfa81898f6eef8fb1abe463e5253cea5bc17f3f"

View file

@ -393,7 +393,7 @@ class PageTarget {
this._videoRecordingInfo = undefined; this._videoRecordingInfo = undefined;
this._screencastRecordingInfo = undefined; this._screencastRecordingInfo = undefined;
this._dialogs = new Map(); this._dialogs = new Map();
this.forcedColors = 'no-override'; this.forcedColors = 'none';
this.disableCache = false; this.disableCache = false;
this.mediumOverride = ''; this.mediumOverride = '';
this.crossProcessCookie = { this.crossProcessCookie = {
@ -635,7 +635,8 @@ class PageTarget {
} }
updateForcedColorsOverride(browsingContext = undefined) { updateForcedColorsOverride(browsingContext = undefined) {
(browsingContext || this._linkedBrowser.browsingContext).forcedColorsOverride = (this.forcedColors !== 'no-override' ? this.forcedColors : this._browserContext.forcedColors) || 'no-override'; const isActive = this.forcedColors === 'active' || this._browserContext.forcedColors === 'active';
(browsingContext || this._linkedBrowser.browsingContext).forcedColorsOverride = isActive ? 'active' : 'none';
} }
async setInterceptFileChooserDialog(enabled) { async setInterceptFileChooserDialog(enabled) {
@ -858,8 +859,8 @@ function fromProtocolReducedMotion(reducedMotion) {
function fromProtocolForcedColors(forcedColors) { function fromProtocolForcedColors(forcedColors) {
if (forcedColors === 'active' || forcedColors === 'none') if (forcedColors === 'active' || forcedColors === 'none')
return forcedColors; return forcedColors;
if (forcedColors === null) if (!forcedColors)
return undefined; return 'none';
throw new Error('Unknown forced colors: ' + forcedColors); throw new Error('Unknown forced colors: ' + forcedColors);
} }
@ -893,7 +894,7 @@ class BrowserContext {
this.forceOffline = false; this.forceOffline = false;
this.disableCache = false; this.disableCache = false;
this.colorScheme = 'none'; this.colorScheme = 'none';
this.forcedColors = 'no-override'; this.forcedColors = 'none';
this.reducedMotion = 'none'; this.reducedMotion = 'none';
this.videoRecordingOptions = undefined; this.videoRecordingOptions = undefined;
this.crossProcessCookie = { this.crossProcessCookie = {

View file

@ -105,7 +105,10 @@ class Juggler {
}; };
// Force create hidden window here, otherwise its creation later closes the web socket! // Force create hidden window here, otherwise its creation later closes the web socket!
Services.appShell.hiddenDOMWindow; // Since https://phabricator.services.mozilla.com/D219834, hiddenDOMWindow is only available on MacOS.
if (Services.appShell.hasHiddenWindow) {
Services.appShell.hiddenDOMWindow;
}
let pipeStopped = false; let pipeStopped = false;
let browserHandler; let browserHandler;

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
REMOTE_URL="https://github.com/WebKit/WebKit.git" REMOTE_URL="https://github.com/WebKit/WebKit.git"
BASE_BRANCH="main" BASE_BRANCH="main"
BASE_REVISION="8ceb1da47e75a488ae4c12017a861636904acd4f" BASE_REVISION="76c95d6131edd36775a5eac01e297926fc974be8"

View file

@ -33,6 +33,7 @@
#import <WebKit/WKUserContentControllerPrivate.h> #import <WebKit/WKUserContentControllerPrivate.h>
#import <WebKit/WKWebViewConfigurationPrivate.h> #import <WebKit/WKWebViewConfigurationPrivate.h>
#import <WebKit/WKWebViewPrivate.h> #import <WebKit/WKWebViewPrivate.h>
#import <WebKit/WKWebpagePreferencesPrivate.h>
#import <WebKit/WKWebsiteDataStorePrivate.h> #import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/WebNSURLExtras.h> #import <WebKit/WebNSURLExtras.h>
#import <WebKit/WebKit.h> #import <WebKit/WebKit.h>
@ -240,6 +241,8 @@ const NSActivityOptions ActivityOptions =
configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO; configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO;
configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO; configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO;
configuration.preferences._domTimersThrottlingEnabled = NO; configuration.preferences._domTimersThrottlingEnabled = NO;
// Do not auto play audio and video with sound.
configuration.defaultWebpagePreferences._autoplayPolicy = _WKWebsiteAutoplayPolicyAllowWithoutSound;
_WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease]; _WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease];
processConfiguration.forceOverlayScrollbars = YES; processConfiguration.forceOverlayScrollbars = YES;
configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease]; configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease];

File diff suppressed because it is too large Load diff

View file

@ -864,31 +864,6 @@ If [`param: expression`] throws or rejects, this method throws.
**Usage** **Usage**
```js
const tweets = page.locator('.tweet .retweets');
expect(await tweets.evaluate(node => node.innerText)).toBe('10 retweets');
```
```java
Locator tweets = page.locator(".tweet .retweets");
assertEquals("10 retweets", tweets.evaluate("node => node.innerText"));
```
```python async
tweets = page.locator(".tweet .retweets")
assert await tweets.evaluate("node => node.innerText") == "10 retweets"
```
```python sync
tweets = page.locator(".tweet .retweets")
assert tweets.evaluate("node => node.innerText") == "10 retweets"
```
```csharp
var tweets = page.Locator(".tweet .retweets");
Assert.AreEqual("10 retweets", await tweets.EvaluateAsync("node => node.innerText"));
```
### param: Locator.evaluate.expression = %%-evaluate-expression-%% ### param: Locator.evaluate.expression = %%-evaluate-expression-%%
* since: v1.14 * since: v1.14

View file

@ -2250,7 +2250,6 @@ Asserts that the target element matches the given [accessibility snapshot](../ar
```js ```js
await expect(page.locator('body')).toMatchAriaSnapshot(); await expect(page.locator('body')).toMatchAriaSnapshot();
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' }); await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
``` ```
```python async ```python async

View file

@ -323,17 +323,18 @@ expect(page).to_have_url(re.compile(".*checkout"))
await Expect(Page).ToHaveURLAsync(new Regex(".*checkout")); await Expect(Page).ToHaveURLAsync(new Regex(".*checkout"));
``` ```
### param: PageAssertions.toHaveURL.urlOrRegExp ### param: PageAssertions.toHaveURL.url
* since: v1.18 * since: v1.18
- `urlOrRegExp` <[string]|[RegExp]> - `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
Expected URL string or RegExp. Expected URL string, RegExp, or predicate receiving [URL] to match.
When a [`option: Browser.newContext.baseURL`] via the context options was provided and the passed URL is a path, it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
### option: PageAssertions.toHaveURL.ignoreCase ### option: PageAssertions.toHaveURL.ignoreCase
* since: v1.44 * since: v1.44
- `ignoreCase` <[boolean]> - `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression parameter if specified. A provided predicate ignores this flag.
### option: PageAssertions.toHaveURL.timeout = %%-js-assertions-timeout-%% ### option: PageAssertions.toHaveURL.timeout = %%-js-assertions-timeout-%%
* since: v1.18 * since: v1.18

View file

@ -6,6 +6,74 @@ toc_max_heading_level: 2
import LiteYouTube from '@site/src/components/LiteYouTube'; import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.50
### Test runner
* New option [`option: Test.step.timeout`] allows specifying a maximum run time for an individual test step. A timed-out step will fail the execution of the test.
```js
test('some test', async ({ page }) => {
await test.step('a step', async () => {
// This step can time out separately from the test
}, { timeout: 1000 });
});
```
* New method [`method: Test.step.skip`] to disable execution of a test step.
```js
test('some test', async ({ page }) => {
await test.step('before running step', async () => {
// Normal step
});
await test.step.skip('not yet ready', async () => {
// This step is skipped
});
await test.step('after running step', async () => {
// This step still runs even though the previous one was skipped
});
});
```
* Expanded [`method: LocatorAssertions.toMatchAriaSnapshot#2`] to allow storing of aria snapshots in separate YAML files.
* Added method [`method: LocatorAssertions.toHaveAccessibleErrorMessage`] to assert the Locator points to an element with a given [aria errormessage](https://w3c.github.io/aria/#aria-errormessage).
* Option [`property: TestConfig.updateSnapshots`] added the configuration enum `changed`. `changed` updates only the snapshots that have changed, whereas `all` now updates all snapshots, regardless of whether there are any differences.
* New option [`property: TestConfig.updateSourceMethod`] defines the way source code is updated when [`property: TestConfig.updateSnapshots`] is configured. Added `overwrite` and `3-way` modes that write the changes into source code, on top of existing `patch` mode that creates a patch file.
```bash
npx playwright test --update-snapshots=changed --update-source-method=3way
```
* Option [`property: TestConfig.webServer`] added a `gracefulShutdown` field for specifying a process kill signal other than the default `SIGKILL`.
* Exposed [`property: TestStep.attachments`] from the reporter API to allow retrieval of all attachments created by that step.
### UI updates
* Updated default HTML reporter to improve display of attachments.
* New button for picking elements to produce aria snapshots.
* Additional details (such as keys pressed) are now displayed alongside action API calls in traces.
* Display of `canvas` content in traces is error-prone. Display is now disabled by default, and can be enabled via the `Display canvas content` UI setting.
* `Call` and `Network` panels now display additional time information.
### Breaking
* [`method: LocatorAssertions.toBeEditable`] and [`method: Locator.isEditable`] now throw if the target element is not `<input>`, `<select>`, or a number of other editable elements.
* Option [`property: TestConfig.updateSnapshots`] now updates all snapshots when set to `all`, rather than only the failed/changed snapshots. Use the new enum `changed` to keep the old functionality of only updating the changed snapshots.
### Browser Versions
* Chromium 133.0.6943.16
* Mozilla Firefox 134.0
* WebKit 18.2
This version was also tested against the following stable channels:
* Google Chrome 132
* Microsoft Edge 132
## Version 1.49 ## Version 1.49
<LiteYouTube <LiteYouTube

View file

@ -629,7 +629,7 @@ export default defineConfig({
- `stdout` ?<["pipe"|"ignore"]> 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` ?<["pipe"|"ignore"]> 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` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. - `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. - `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
- `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGINT` and `SIGTERM` signals, so this option is ignored. - `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting down a Docker container requires `SIGTERM`.
- `signal` <["SIGINT"|"SIGTERM"]> - `signal` <["SIGINT"|"SIGTERM"]>
- `timeout` <[int]> - `timeout` <[int]>
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified. - `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.

View file

@ -111,5 +111,6 @@ Complete set of Playwright Test options is available in the [configuration file]
| `--ui-host <host>` | Host to serve UI on; specifying this option opens UI in a browser tab. | | `--ui-host <host>` | Host to serve UI on; specifying this option opens UI in a browser tab. |
| `--ui-port <port>` | Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab. | | `--ui-port <port>` | Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab. |
| `-u` or `--update-snapshots [mode]` | Update snapshots with actual results. Possible values are "all", "changed", "missing", and "none". Not passing defaults to "missing"; passing without a value defaults to "changed". | | `-u` or `--update-snapshots [mode]` | Update snapshots with actual results. Possible values are "all", "changed", "missing", and "none". Not passing defaults to "missing"; passing without a value defaults to "changed". |
| `--update-source-method [mode]` | Update snapshots with actual results. Possible values are "patch" (default), "3way" and "overwrite". "Patch" creates a unified diff file that can be used to update the source code later. "3way" generates merge conflict markers in source code. "Overwrite" overwrites the source code with the new snapshot values.|
| `-j <workers>` or `--workers <workers>` | Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%). | | `-j <workers>` or `--workers <workers>` | Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%). |
| `-x` | Stop after the first failure. | | `-x` | Stop after the first failure. |

View file

@ -407,7 +407,7 @@ Automatic fixtures are set up for each test/worker, even when the test does not
Here is an example fixture that automatically attaches debug logs when the test fails, so we can later review the logs in the reporter. Note how it uses [TestInfo] object that is available in each test/fixture to retrieve metadata about the test being run. Here is an example fixture that automatically attaches debug logs when the test fails, so we can later review the logs in the reporter. Note how it uses [TestInfo] object that is available in each test/fixture to retrieve metadata about the test being run.
```js title="my-test.ts" ```js title="my-test.ts"
import * as debug from 'debug'; import debug from 'debug';
import * as fs from 'fs'; import * as fs from 'fs';
import { test as base } from '@playwright/test'; import { test as base } from '@playwright/test';

View file

@ -37,7 +37,7 @@ export default defineConfig({
| `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. |
| `gracefulShutdown` | How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGINT` and `SIGTERM` signals, so this option is ignored. | | `gracefulShutdown` | How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting down a Docker container requires `SIGTERM`. |
## Adding a server timeout ## Adding a server timeout

18
package-lock.json generated
View file

@ -61,7 +61,7 @@
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"ssim.js": "^3.5.0", "ssim.js": "^3.5.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^5.4.6", "vite": "^5.4.14",
"ws": "^8.17.1", "ws": "^8.17.1",
"xml2js": "^0.5.0", "xml2js": "^0.5.0",
"yaml": "^2.6.0" "yaml": "^2.6.0"
@ -6931,10 +6931,11 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.28.4", "version": "5.28.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@fastify/busboy": "^2.0.0" "@fastify/busboy": "^2.0.0"
}, },
@ -7027,9 +7028,10 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.6", "version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
@ -7833,7 +7835,7 @@
"dependencies": { "dependencies": {
"playwright": "1.51.0-next", "playwright": "1.51.0-next",
"playwright-core": "1.51.0-next", "playwright-core": "1.51.0-next",
"vite": "^5.2.8" "vite": "^5.4.14"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"

View file

@ -100,7 +100,7 @@
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"ssim.js": "^3.5.0", "ssim.js": "^3.5.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^5.4.6", "vite": "^5.4.14",
"ws": "^8.17.1", "ws": "^8.17.1",
"xml2js": "^0.5.0", "xml2js": "^0.5.0",
"yaml": "^2.6.0" "yaml": "^2.6.0"

View file

@ -3,31 +3,31 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1155", "revision": "1156",
"installByDefault": true, "installByDefault": true,
"browserVersion": "133.0.6943.16" "browserVersion": "133.0.6943.27"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",
"revision": "1293", "revision": "1297",
"installByDefault": false, "installByDefault": false,
"browserVersion": "133.0.6943.0" "browserVersion": "134.0.6974.0"
}, },
{ {
"name": "firefox", "name": "firefox",
"revision": "1471", "revision": "1472",
"installByDefault": true, "installByDefault": true,
"browserVersion": "134.0" "browserVersion": "134.0"
}, },
{ {
"name": "firefox-beta", "name": "firefox-beta",
"revision": "1467", "revision": "1468",
"installByDefault": false, "installByDefault": false,
"browserVersion": "133.0b9" "browserVersion": "133.0b9"
}, },
{ {
"name": "webkit", "name": "webkit",
"revision": "2123", "revision": "2125",
"installByDefault": true, "installByDefault": true,
"revisionOverrides": { "revisionOverrides": {
"debian11-x64": "2105", "debian11-x64": "2105",

View file

@ -118,7 +118,14 @@ export class PlaywrightConnection {
const playwright = createPlaywright({ sdkLanguage: options.sdkLanguage, isServer: true }); const playwright = createPlaywright({ sdkLanguage: options.sdkLanguage, isServer: true });
const ownedSocksProxy = await this._createOwnedSocksProxy(playwright); const ownedSocksProxy = await this._createOwnedSocksProxy(playwright);
const browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions); let browserName = this._options.browserName;
if ('bidi' === browserName) {
if (this._options.launchOptions?.channel?.toLocaleLowerCase().includes('firefox'))
browserName = 'bidiFirefox';
else
browserName = 'bidiChromium';
}
const browser = await playwright[browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions);
this._cleanups.push(async () => { this._cleanups.push(async () => {
for (const browser of playwright.allBrowsers()) for (const browser of playwright.allBrowsers())

View file

@ -139,6 +139,9 @@ function defaultProfilePreferences(
'dom.min_background_timeout_value_without_budget_throttling': 0, 'dom.min_background_timeout_value_without_budget_throttling': 0,
'dom.timeout.enable_budget_timer_throttling': false, 'dom.timeout.enable_budget_timer_throttling': false,
// Disable HTTPS-First upgrades
'dom.security.https_first': false,
// Only load extensions from the application and user profile // Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
'extensions.autoDisableScopes': 0, 'extensions.autoDisableScopes': 0,

View file

@ -38,7 +38,10 @@ export const chromiumSwitches = [
// ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230 // ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230
// LensOverlay - Hides the Lens feature in the URL address bar. Its not working in unofficial builds. // LensOverlay - Hides the Lens feature in the URL address bar. Its not working in unofficial builds.
// PlzDedicatedWorker - https://github.com/microsoft/playwright/issues/31747 // PlzDedicatedWorker - https://github.com/microsoft/playwright/issues/31747
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker', // DeferRendererTasksAfterInput - this makes Page.frameScheduledNavigation arrive much later after a click,
// making our navigation auto-wait after click not working. Can be removed once we deperecate noWaitAfter.
// See https://github.com/microsoft/playwright/pull/34372.
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker,DeferRendererTasksAfterInput',
'--allow-pre-commit-input', '--allow-pre-commit-input',
'--disable-hang-monitor', '--disable-hang-monitor',
'--disable-ipc-flooding-protection', '--disable-ipc-flooding-protection',

View file

@ -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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 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/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36 Edg/133.0.6943.27",
"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/133.0.6943.16 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36 Edg/133.0.6943.27",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080

View file

@ -301,6 +301,11 @@ export module Protocol {
forcedColors: ("active"|"none")|null; forcedColors: ("active"|"none")|null;
}; };
export type setForcedColorsReturnValue = void; export type setForcedColorsReturnValue = void;
export type setContrastParameters = {
browserContextId?: string;
contrast: ("less"|"more"|"custom"|"no-preference")|null;
};
export type setContrastReturnValue = void;
export type setVideoRecordingOptionsParameters = { export type setVideoRecordingOptionsParameters = {
browserContextId?: string; browserContextId?: string;
options?: { options?: {
@ -530,6 +535,7 @@ export module Protocol {
colorScheme?: ("dark"|"light"|"no-preference"); colorScheme?: ("dark"|"light"|"no-preference");
reducedMotion?: ("reduce"|"no-preference"); reducedMotion?: ("reduce"|"no-preference");
forcedColors?: ("active"|"none"); forcedColors?: ("active"|"none");
contrast?: ("less"|"more"|"custom"|"no-preference");
}; };
export type setEmulatedMediaReturnValue = void; export type setEmulatedMediaReturnValue = void;
export type setCacheDisabledParameters = { export type setCacheDisabledParameters = {
@ -1131,6 +1137,7 @@ export module Protocol {
"Browser.setColorScheme": Browser.setColorSchemeParameters; "Browser.setColorScheme": Browser.setColorSchemeParameters;
"Browser.setReducedMotion": Browser.setReducedMotionParameters; "Browser.setReducedMotion": Browser.setReducedMotionParameters;
"Browser.setForcedColors": Browser.setForcedColorsParameters; "Browser.setForcedColors": Browser.setForcedColorsParameters;
"Browser.setContrast": Browser.setContrastParameters;
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsParameters; "Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsParameters;
"Browser.cancelDownload": Browser.cancelDownloadParameters; "Browser.cancelDownload": Browser.cancelDownloadParameters;
"Heap.collectGarbage": Heap.collectGarbageParameters; "Heap.collectGarbage": Heap.collectGarbageParameters;
@ -1213,6 +1220,7 @@ export module Protocol {
"Browser.setColorScheme": Browser.setColorSchemeReturnValue; "Browser.setColorScheme": Browser.setColorSchemeReturnValue;
"Browser.setReducedMotion": Browser.setReducedMotionReturnValue; "Browser.setReducedMotion": Browser.setReducedMotionReturnValue;
"Browser.setForcedColors": Browser.setForcedColorsReturnValue; "Browser.setForcedColors": Browser.setForcedColorsReturnValue;
"Browser.setContrast": Browser.setContrastReturnValue;
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsReturnValue; "Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsReturnValue;
"Browser.cancelDownload": Browser.cancelDownloadReturnValue; "Browser.cancelDownload": Browser.cancelDownloadReturnValue;
"Heap.collectGarbage": Heap.collectGarbageReturnValue; "Heap.collectGarbage": Heap.collectGarbageReturnValue;

View file

@ -1404,8 +1404,6 @@ export class InjectedScript {
received = getAriaRole(element) || ''; received = getAriaRole(element) || '';
} else if (expression === 'to.have.title') { } else if (expression === 'to.have.title') {
received = this.document.title; received = this.document.title;
} else if (expression === 'to.have.url') {
received = this.document.location.href;
} else if (expression === 'to.have.value') { } else if (expression === 'to.have.value') {
element = this.retarget(element, 'follow-label')!; element = this.retarget(element, 'follow-label')!;
if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT') if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')

View file

@ -214,12 +214,6 @@ export class HttpServer {
} }
_handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean { _handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean {
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader('Access-Control-Request-Method', '*');
response.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
if (request.headers.origin)
response.setHeader('Access-Control-Allow-Headers', request.headers.origin);
if (request.method === 'OPTIONS') { if (request.method === 'OPTIONS') {
response.writeHead(200); response.writeHead(200);
response.end(); response.end();

View file

@ -12176,12 +12176,6 @@ export interface Locator {
* rejects, this method throws. * rejects, this method throws.
* *
* **Usage** * **Usage**
*
* ```js
* const tweets = page.locator('.tweet .retweets');
* expect(await tweets.evaluate(node => node.innerText)).toBe('10 retweets');
* ```
*
* @param pageFunction Function to be evaluated in the page context. * @param pageFunction Function to be evaluated in the page context.
* @param arg Optional argument to pass to * @param arg Optional argument to pass to
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression). * [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression).
@ -12207,12 +12201,6 @@ export interface Locator {
* rejects, this method throws. * rejects, this method throws.
* *
* **Usage** * **Usage**
*
* ```js
* const tweets = page.locator('.tweet .retweets');
* expect(await tweets.evaluate(node => node.innerText)).toBe('10 retweets');
* ```
*
* @param pageFunction Function to be evaluated in the page context. * @param pageFunction Function to be evaluated in the page context.
* @param arg Optional argument to pass to * @param arg Optional argument to pass to
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression). * [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression).

View file

@ -27,7 +27,7 @@
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.51.0-next", "playwright-core": "1.51.0-next",
"vite": "^5.2.8", "vite": "^5.4.14",
"playwright": "1.51.0-next" "playwright": "1.51.0-next"
} }
} }

View file

@ -240,8 +240,8 @@ function validateConfig(file: string, config: Config) {
} }
if ('updateSnapshots' in config && config.updateSnapshots !== undefined) { if ('updateSnapshots' in config && config.updateSnapshots !== undefined) {
if (typeof config.updateSnapshots !== 'string' || !['all', 'none', 'missing'].includes(config.updateSnapshots)) if (typeof config.updateSnapshots !== 'string' || !['all', 'changed', 'missing', 'none'].includes(config.updateSnapshots))
throw errorWithFile(file, `config.updateSnapshots must be one of "all", "none" or "missing"`); throw errorWithFile(file, `config.updateSnapshots must be one of "all", "changed", "missing" or "none"`);
} }
if ('workers' in config && config.workers !== undefined) { if ('workers' in config && config.workers !== undefined) {

View file

@ -21,11 +21,12 @@ import { expectTypes, callLogText } from '../util';
import { toBeTruthy } from './toBeTruthy'; import { toBeTruthy } from './toBeTruthy';
import { toEqual } from './toEqual'; import { toEqual } from './toEqual';
import { toMatchText } from './toMatchText'; import { toMatchText } from './toMatchText';
import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils'; import { isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils';
import { currentTestInfo } from '../common/globals'; import { currentTestInfo } from '../common/globals';
import { TestInfoImpl } from '../worker/testInfo'; import { TestInfoImpl } from '../worker/testInfo';
import type { ExpectMatcherState } from '../../types/test'; import type { ExpectMatcherState } from '../../types/test';
import { takeFirst } from '../common/config'; import { takeFirst } from '../common/config';
import { toHaveURL as toHaveURLExternal } from './toHaveURL';
export interface LocatorEx extends Locator { export interface LocatorEx extends Locator {
_expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>; _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
@ -382,16 +383,10 @@ export function toHaveTitle(
export function toHaveURL( export function toHaveURL(
this: ExpectMatcherState, this: ExpectMatcherState,
page: Page, page: Page,
expected: string | RegExp, expected: string | RegExp | ((url: URL) => boolean),
options?: { ignoreCase?: boolean, timeout?: number }, options?: { ignoreCase?: boolean; timeout?: number },
) { ) {
const baseURL = (page.context() as any)._options.baseURL; return toHaveURLExternal.call(this, page, expected, options);
expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected;
const locator = page.locator(':root') as LocatorEx;
return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
return await locator._expect('to.have.url', { expectedText, isNot, timeout });
}, expected, options);
} }
export async function toBeOK( export async function toBeOK(

View file

@ -0,0 +1,153 @@
/**
* 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 type { Page } from 'playwright-core';
import type { ExpectMatcherState } from '../../types/test';
import { EXPECTED_COLOR, printReceived } from '../common/expectBundle';
import { matcherHint, type MatcherResult } from './matcherHint';
import { constructURLBasedOnBaseURL, urlMatches } from 'playwright-core/lib/utils';
import { colors } from 'playwright-core/lib/utilsBundle';
import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring } from './expect';
export async function toHaveURL(
this: ExpectMatcherState,
page: Page,
expected: string | RegExp | ((url: URL) => boolean),
options?: { ignoreCase?: boolean; timeout?: number },
): Promise<MatcherResult<string | RegExp, string>> {
const matcherName = 'toHaveURL';
const expression = 'page';
const matcherOptions = {
isNot: this.isNot,
promise: this.promise,
};
if (
!(typeof expected === 'string') &&
!(expected && 'test' in expected && typeof expected.test === 'function') &&
!(typeof expected === 'function')
) {
throw new Error(
[
// Always display `expected` in expectation place
matcherHint(this, undefined, matcherName, expression, undefined, matcherOptions),
`${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected')} value must be a string, regular expression, or predicate`,
this.utils.printWithType('Expected', expected, this.utils.printExpected,),
].join('\n\n'),
);
}
const timeout = options?.timeout ?? this.timeout;
const baseURL: string | undefined = (page.context() as any)._options.baseURL;
let conditionSucceeded = false;
let lastCheckedURLString: string | undefined = undefined;
try {
await page.mainFrame().waitForURL(
url => {
lastCheckedURLString = url.toString();
if (options?.ignoreCase) {
return (
!this.isNot ===
urlMatches(
baseURL?.toLocaleLowerCase(),
lastCheckedURLString.toLocaleLowerCase(),
typeof expected === 'string'
? expected.toLocaleLowerCase()
: expected,
)
);
}
return (
!this.isNot === urlMatches(baseURL, lastCheckedURLString, expected)
);
},
{ timeout },
);
conditionSucceeded = true;
} catch (e) {
conditionSucceeded = false;
}
if (conditionSucceeded)
return { name: matcherName, pass: !this.isNot, message: () => '' };
return {
name: matcherName,
pass: this.isNot,
message: () =>
toHaveURLMessage(
this,
matcherName,
expression,
typeof expected === 'string'
? constructURLBasedOnBaseURL(baseURL, expected)
: expected,
lastCheckedURLString,
this.isNot,
true,
timeout,
),
actual: lastCheckedURLString,
timeout,
};
}
function toHaveURLMessage(
state: ExpectMatcherState,
matcherName: string,
expression: string,
expected: string | RegExp | Function,
received: string | undefined,
pass: boolean,
didTimeout: boolean,
timeout: number,
): string {
const matcherOptions = {
isNot: state.isNot,
promise: state.promise,
};
const receivedString = received || '';
const messagePrefix = matcherHint(state, undefined, matcherName, expression, undefined, matcherOptions, didTimeout ? timeout : undefined);
let printedReceived: string | undefined;
let printedExpected: string | undefined;
let printedDiff: string | undefined;
if (typeof expected === 'function') {
printedExpected = `Expected predicate to ${!state.isNot ? 'succeed' : 'fail'}`;
printedReceived = `Received string: ${printReceived(receivedString)}`;
} else {
if (pass) {
if (typeof expected === 'string') {
printedExpected = `Expected string: not ${state.utils.printExpected(expected)}`;
const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length);
printedReceived = `Received string: ${formattedReceived}`;
} else {
printedExpected = `Expected pattern: not ${state.utils.printExpected(expected)}`;
const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null);
printedReceived = `Received string: ${formattedReceived}`;
}
} else {
const labelExpected = `Expected ${typeof expected === 'string' ? 'string' : 'pattern'}`;
printedDiff = state.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false);
}
}
const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived;
return messagePrefix + resultDetails;
}

View file

@ -30,12 +30,13 @@ import path from 'path';
type ToMatchAriaSnapshotExpected = { type ToMatchAriaSnapshotExpected = {
name?: string; name?: string;
path?: string; path?: string;
timeout?: number;
} | string; } | string;
export async function toMatchAriaSnapshot( export async function toMatchAriaSnapshot(
this: ExpectMatcherState, this: ExpectMatcherState,
receiver: LocatorEx, receiver: LocatorEx,
expectedParam: ToMatchAriaSnapshotExpected, expectedParam?: ToMatchAriaSnapshotExpected,
options: { timeout?: number } = {}, options: { timeout?: number } = {},
): Promise<MatcherResult<string | RegExp, string>> { ): Promise<MatcherResult<string | RegExp, string>> {
const matcherName = 'toMatchAriaSnapshot'; const matcherName = 'toMatchAriaSnapshot';
@ -55,9 +56,11 @@ export async function toMatchAriaSnapshot(
}; };
let expected: string; let expected: string;
let timeout: number;
let expectedPath: string | undefined; let expectedPath: string | undefined;
if (isString(expectedParam)) { if (isString(expectedParam)) {
expected = expectedParam; expected = expectedParam;
timeout = options.timeout ?? this.timeout;
} else { } else {
if (expectedParam?.name) { if (expectedParam?.name) {
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name)); expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
@ -71,6 +74,7 @@ export async function toMatchAriaSnapshot(
expectedPath = testInfo.snapshotPath(sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml'); expectedPath = testInfo.snapshotPath(sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml');
} }
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => ''); expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
timeout = expectedParam?.timeout ?? this.timeout;
} }
const generateMissingBaseline = updateSnapshots === 'missing' && !expected; const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
@ -84,7 +88,6 @@ export async function toMatchAriaSnapshot(
} }
} }
const timeout = options.timeout ?? this.timeout;
expected = unshift(expected); expected = unshift(expected);
const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout }); const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout });
const typedReceived = received as MatcherReceived | typeof kNoElementsFoundError; const typedReceived = received as MatcherReceived | typeof kNoElementsFoundError;
@ -134,7 +137,7 @@ export async function toMatchAriaSnapshot(
} }
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' }; return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
} else { } else {
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`; const suggestedRebaseline = `\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\``;
return { pass: false, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline }; return { pass: false, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
} }
} }

View file

@ -282,11 +282,11 @@ async function mergeReports(reportDir: string | undefined, opts: { [key: string]
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides { function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined; const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined;
let updateSnapshots: 'all' | 'changed' | 'missing' | 'none'; let updateSnapshots: 'all' | 'changed' | 'missing' | 'none' | undefined;
if (['all', 'changed', 'missing', 'none'].includes(options.updateSnapshots)) if (['all', 'changed', 'missing', 'none'].includes(options.updateSnapshots))
updateSnapshots = options.updateSnapshots; updateSnapshots = options.updateSnapshots;
else else
updateSnapshots = 'updateSnapshots' in options ? 'changed' : 'missing'; updateSnapshots = 'updateSnapshots' in options ? 'changed' : undefined;
const overrides: ConfigCLIOverrides = { const overrides: ConfigCLIOverrides = {
forbidOnly: options.forbidOnly ? true : undefined, forbidOnly: options.forbidOnly ? true : undefined,
@ -303,7 +303,7 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid
tsconfig: options.tsconfig ? path.resolve(process.cwd(), options.tsconfig) : undefined, tsconfig: options.tsconfig ? path.resolve(process.cwd(), options.tsconfig) : undefined,
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined, ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
updateSnapshots, updateSnapshots,
updateSourceMethod: options.updateSourceMethod || 'patch', updateSourceMethod: options.updateSourceMethod,
workers: options.workers, workers: options.workers,
}; };

View file

@ -129,6 +129,8 @@ class ListReporter extends TerminalReporter {
if (this._needNewLine) { if (this._needNewLine) {
this._needNewLine = false; this._needNewLine = false;
process.stdout.write('\n'); process.stdout.write('\n');
++this._lastRow;
this._lastColumn = 0;
} }
} }
@ -210,6 +212,7 @@ class ListReporter extends TerminalReporter {
process.stdout.write('\n'); process.stdout.write('\n');
} }
++this._lastRow; ++this._lastRow;
this._lastColumn = 0;
} }
private _updateLine(row: number, text: string, prefix: string) { private _updateLine(row: number, text: string, prefix: string) {

View file

@ -68,24 +68,27 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
traverse(fileNode, { traverse(fileNode, {
CallExpression: path => { CallExpression: path => {
const node = path.node; const node = path.node;
if (node.arguments.length !== 1) if (node.arguments.length < 1)
return; return;
if (!t.isMemberExpression(node.callee)) if (!t.isMemberExpression(node.callee))
return; return;
const argument = node.arguments[0]; const argument = node.arguments[0];
if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument)) if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument))
return; return;
const prop = node.callee.property;
const matcher = node.callee.property; if (!prop.loc || !argument.start || !argument.end)
return;
// Replacements are anchored by the location of the call expression.
// However, replacement text is meant to only replace the first argument.
for (const replacement of replacements) { for (const replacement of replacements) {
// In Babel, rows are 1-based, columns are 0-based. // In Babel, rows are 1-based, columns are 0-based.
if (matcher.loc!.start.line !== replacement.location.line) if (prop.loc.start.line !== replacement.location.line)
continue; continue;
if (matcher.loc!.start.column + 1 !== replacement.location.column) if (prop.loc.start.column + 1 !== replacement.location.column)
continue; continue;
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0]; const indent = lines[prop.loc.start.line - 1].match(/^\s*/)![0];
const newText = replacement.code.replace(/\{indent\}/g, indent); const newText = replacement.code.replace(/\{indent\}/g, indent);
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText }); ranges.push({ start: argument.start, end: argument.end, oldText: source.substring(argument.start, argument.end), newText });
// We can have multiple, hopefully equal, replacements for the same location, // We can have multiple, hopefully equal, replacements for the same location,
// for example when a single test runs multiple times because of projects or retries. // for example when a single test runs multiple times because of projects or retries.
// Do not apply multiple replacements for the same assertion. // Do not apply multiple replacements for the same assertion.

View file

@ -322,7 +322,7 @@ export class TestInfoImpl implements TestInfo {
location: data.location, location: data.location,
}; };
this._onStepBegin(payload); this._onStepBegin(payload);
this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.apiName || data.title, data.params, data.location ? [data.location] : []); this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.category, data.apiName || data.title, data.params, data.location ? [data.location] : []);
return step; return step;
} }
@ -421,7 +421,7 @@ export class TestInfoImpl implements TestInfo {
} else { } else {
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action // trace viewer has no means of representing attachments outside of a step, so we create an artificial action
const callId = `attach@${++this._lastStepId}`; const callId = `attach@${++this._lastStepId}`;
this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, `attach "${attachment.name}"`, undefined, []); this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, 'attach', `attach "${attachment.name}"`, undefined, []);
this._tracing.appendAfterActionForStep(callId, undefined, [attachment]); this._tracing.appendAfterActionForStep(callId, undefined, [attachment]);
} }

View file

@ -245,14 +245,14 @@ export class TestTracing {
}); });
} }
appendBeforeActionForStep(callId: string, parentId: string | undefined, apiName: string, params: Record<string, any> | undefined, stack: StackFrame[]) { appendBeforeActionForStep(callId: string, parentId: string | undefined, category: string, apiName: string, params: Record<string, any> | undefined, stack: StackFrame[]) {
this._appendTraceEvent({ this._appendTraceEvent({
type: 'before', type: 'before',
callId, callId,
parentId, parentId,
startTime: monotonicTime(), startTime: monotonicTime(),
class: 'Test', class: 'Test',
method: 'step', method: category,
apiName, apiName,
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])), params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
stack, stack,

View file

@ -75,16 +75,20 @@ export class WorkerMain extends ProcessRunner {
process.on('unhandledRejection', reason => this.unhandledError(reason)); process.on('unhandledRejection', reason => this.unhandledError(reason));
process.on('uncaughtException', error => this.unhandledError(error)); process.on('uncaughtException', error => this.unhandledError(error));
process.stdout.write = (chunk: string | Buffer) => { process.stdout.write = (chunk: string | Buffer, cb?: any) => {
this.dispatchEvent('stdOut', stdioChunkToParams(chunk)); this.dispatchEvent('stdOut', stdioChunkToParams(chunk));
this._currentTest?._tracing.appendStdioToTrace('stdout', chunk); this._currentTest?._tracing.appendStdioToTrace('stdout', chunk);
if (typeof cb === 'function')
process.nextTick(cb);
return true; return true;
}; };
if (!process.env.PW_RUNNER_DEBUG) { if (!process.env.PW_RUNNER_DEBUG) {
process.stderr.write = (chunk: string | Buffer) => { process.stderr.write = (chunk: string | Buffer, cb?: any) => {
this.dispatchEvent('stdErr', stdioChunkToParams(chunk)); this.dispatchEvent('stdErr', stdioChunkToParams(chunk));
this._currentTest?._tracing.appendStdioToTrace('stderr', chunk); this._currentTest?._tracing.appendStdioToTrace('stderr', chunk);
if (typeof cb === 'function')
process.nextTick(cb);
return true; return true;
}; };
} }

View file

@ -8714,7 +8714,6 @@ interface LocatorAssertions {
* ```js * ```js
* await expect(page.locator('body')).toMatchAriaSnapshot(); * await expect(page.locator('body')).toMatchAriaSnapshot();
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' }); * await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
* await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
* ``` * ```
* *
* @param options * @param options
@ -8819,14 +8818,18 @@ interface PageAssertions {
* await expect(page).toHaveURL(/.*checkout/); * await expect(page).toHaveURL(/.*checkout/);
* ``` * ```
* *
* @param urlOrRegExp Expected URL string or RegExp. * @param url Expected URL string, RegExp, or predicate receiving [URL] to match. When a
* [`baseURL`](https://playwright.dev/docs/api/class-browser#browser-new-context-option-base-url) via the context
* options was provided and the passed URL is a path, it gets merged via the
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
* @param options * @param options
*/ */
toHaveURL(urlOrRegExp: string|RegExp, options?: { toHaveURL(url: string|RegExp|((url: URL) => boolean), options?: {
/** /**
* Whether to perform case-insensitive match. * Whether to perform case-insensitive match.
* [`ignoreCase`](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-url-option-ignore-case) * [`ignoreCase`](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-url-option-ignore-case)
* option takes precedence over the corresponding regular expression flag if specified. * option takes precedence over the corresponding regular expression parameter if specified. A provided predicate
* ignores this flag.
*/ */
ignoreCase?: boolean; ignoreCase?: boolean;
@ -9669,9 +9672,10 @@ interface TestConfigWebServer {
/** /**
* How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: * How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal:
* 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit * 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit
* within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't * within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent.
* support `SIGINT` and `SIGTERM` signals, so this option is ignored. * Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting
* down a Docker container requires `SIGTERM`.
*/ */
gracefulShutdown?: { gracefulShutdown?: {
signal: "SIGINT"|"SIGTERM"; signal: "SIGINT"|"SIGTERM";

View file

@ -41,6 +41,10 @@
color: var(--vscode-editorCodeLens-foreground); color: var(--vscode-editorCodeLens-foreground);
} }
.action-skipped {
margin-right: 4px;
}
.action-icon { .action-icon {
flex: none; flex: none;
display: flex; display: flex;

View file

@ -15,7 +15,7 @@
*/ */
import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace'; import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace';
import { msToString } from '@web/uiUtils'; import { clsx, msToString } from '@web/uiUtils';
import * as React from 'react'; import * as React from 'react';
import './actionList.css'; import './actionList.css';
import * as modelUtil from './modelUtil'; import * as modelUtil from './modelUtil';
@ -25,6 +25,7 @@ import { TreeView } from '@web/components/treeView';
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil'; import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
import type { Boundaries } from './geometry'; import type { Boundaries } from './geometry';
import { ToolbarButton } from '@web/components/toolbarButton'; import { ToolbarButton } from '@web/components/toolbarButton';
import { testStatusIcon } from './testUtils';
export interface ActionListProps { export interface ActionListProps {
actions: ActionTraceEventInContext[], actions: ActionTraceEventInContext[],
@ -119,6 +120,7 @@ export const renderAction = (
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript'); const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
const isSkipped = action.class === 'Test' && action.method === 'test.step.skip';
let time: string = ''; let time: string = '';
if (action.endTime) if (action.endTime)
time = msToString(action.endTime - action.startTime); time = msToString(action.endTime - action.startTime);
@ -149,9 +151,10 @@ export const renderAction = (
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>} {action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
{action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>} {action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>}
</div> </div>
{(showDuration || showBadges || showAttachments) && <div className='spacer'></div>} {(showDuration || showBadges || showAttachments || isSkipped) && <div className='spacer'></div>}
{showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={() => revealAttachment(action.attachments![0])} />} {showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={() => revealAttachment(action.attachments![0])} />}
{showDuration && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>} {showDuration && !isSkipped && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>}
{isSkipped && <span className={clsx('action-skipped', 'codicon', testStatusIcon('skipped'))} title='skipped'></span>}
{showBadges && <div className='action-icons' onClick={() => revealConsole?.()}> {showBadges && <div className='action-icons' onClick={() => revealConsole?.()}>
{!!errors && <div className='action-icon'><span className='codicon codicon-error'></span><span className='action-icon-value'>{errors}</span></div>} {!!errors && <div className='action-icon'><span className='codicon codicon-error'></span><span className='action-icon-value'>{errors}</span></div>}
{!!warnings && <div className='action-icon'><span className='codicon codicon-warning'></span><span className='action-icon-value'>{warnings}</span></div>} {!!warnings && <div className='action-icon'><span className='codicon codicon-warning'></span><span className='action-icon-value'>{warnings}</span></div>}

View file

@ -10,13 +10,13 @@
let textarea = document.querySelector('textarea'); let textarea = document.querySelector('textarea');
textarea.focus(); textarea.focus();
textarea.addEventListener('keydown', event => { textarea.addEventListener('keydown', event => {
log('Keydown:', event.key, event.code, event.which, modifiers(event)); log('Keydown:', event.key, event.code, getLocation(event), modifiers(event));
}); });
textarea.addEventListener('keypress', event => { textarea.addEventListener('keypress', event => {
log('Keypress:', event.key, event.code, event.which, event.charCode, modifiers(event)); log('Keypress:', event.key, event.code, getLocation(event), event.charCode, modifiers(event));
}); });
textarea.addEventListener('keyup', event => { textarea.addEventListener('keyup', event => {
log('Keyup:', event.key, event.code, event.which, modifiers(event)); log('Keyup:', event.key, event.code, getLocation(event), modifiers(event));
}); });
function modifiers(event) { function modifiers(event) {
let m = []; let m = [];
@ -28,6 +28,15 @@
m.push('Shift') m.push('Shift')
return '[' + m.join(' ') + ']'; return '[' + m.join(' ') + ']';
} }
function getLocation(event) {
switch (event.location) {
case KeyboardEvent.DOM_KEY_LOCATION_STANDARD: return 'STANDARD';
case KeyboardEvent.DOM_KEY_LOCATION_LEFT: return 'LEFT';
case KeyboardEvent.DOM_KEY_LOCATION_RIGHT: return 'RIGHT';
case KeyboardEvent.DOM_KEY_LOCATION_NUMPAD: return 'NUMPAD';
default: return 'Unknown: ' + event.location;
};
}
function log(...args) { function log(...args) {
console.log.apply(console, args); console.log.apply(console, args);
result += args.join(' ') + '\n'; result += args.join(' ') + '\n';

View file

@ -58,6 +58,8 @@ class ExpectationReporter implements Reporter {
const key = test.titlePath().slice(2).join(' '); const key = test.titlePath().slice(2).join(' ');
if (outcome === 'timeout') if (outcome === 'timeout')
expectations.set(key, outcome); expectations.set(key, outcome);
else if (expectations.has(key) && test.outcome() !== 'skipped')
expectations.delete(key); // Remove tests that no longer timeout.
} }
const keys = Array.from(expectations.keys()); const keys = Array.from(expectations.keys());
keys.sort(); keys.sort();

View file

@ -15,43 +15,8 @@ library/browsercontext-page-event.spec.ts should have about:blank for empty
library/browsercontext-proxy.spec.ts should use proxy for https urls [timeout] library/browsercontext-proxy.spec.ts should use proxy for https urls [timeout]
library/browsercontext-service-worker-policy.spec.ts block blocks service worker registration [timeout] library/browsercontext-service-worker-policy.spec.ts block blocks service worker registration [timeout]
library/browsercontext-timezone-id.spec.ts should work for multiple pages sharing same process [timeout] library/browsercontext-timezone-id.spec.ts should work for multiple pages sharing same process [timeout]
library/browsertype-connect.spec.ts launchServer only should be able to reconnect to a browser 12 times without warnings [timeout]
library/browsertype-connect.spec.ts launchServer only should properly disconnect when connection closes from the server side [timeout]
library/browsertype-connect.spec.ts launchServer only should work with cluster [timeout]
library/browsertype-connect.spec.ts launchServer disconnected event should be emitted when browser is closed or server is closed [timeout]
library/browsertype-connect.spec.ts launchServer disconnected event should have browser as argument [timeout]
library/browsertype-connect.spec.ts launchServer setInputFiles should preserve lastModified timestamp [timeout]
library/browsertype-connect.spec.ts launchServer should be able to connect 20 times to a single server without warnings [timeout] library/browsertype-connect.spec.ts launchServer should be able to connect 20 times to a single server without warnings [timeout]
library/browsertype-connect.spec.ts launchServer should be able to connect two browsers at the same time [timeout]
library/browsertype-connect.spec.ts launchServer should be able to connect when the wsEndpoint is passed as an option [timeout]
library/browsertype-connect.spec.ts launchServer should be able to reconnect to a browser [timeout]
library/browsertype-connect.spec.ts launchServer should be able to visit ipv6 [timeout]
library/browsertype-connect.spec.ts launchServer should be able to visit ipv6 through localhost [timeout]
library/browsertype-connect.spec.ts launchServer should connect over http [timeout]
library/browsertype-connect.spec.ts launchServer should connect over wss [timeout]
library/browsertype-connect.spec.ts launchServer should emit close events on pages and contexts [timeout]
library/browsertype-connect.spec.ts launchServer should error when saving download after deletion [timeout]
library/browsertype-connect.spec.ts launchServer should filter launch options [timeout]
library/browsertype-connect.spec.ts launchServer should fulfill with global fetch result [timeout]
library/browsertype-connect.spec.ts launchServer should handle exceptions during connect [timeout]
library/browsertype-connect.spec.ts launchServer should ignore page.pause when headed [timeout]
library/browsertype-connect.spec.ts launchServer should not throw on close after disconnect [timeout]
library/browsertype-connect.spec.ts launchServer should properly disconnect when connection closes from the client side [timeout]
library/browsertype-connect.spec.ts launchServer should record trace with sources [timeout]
library/browsertype-connect.spec.ts launchServer should reject navigation when browser closes [timeout]
library/browsertype-connect.spec.ts launchServer should reject waitForEvent before browser.close finishes [timeout]
library/browsertype-connect.spec.ts launchServer should reject waitForEvent before browser.onDisconnect fires [timeout]
library/browsertype-connect.spec.ts launchServer should reject waitForSelector when browser closes [timeout]
library/browsertype-connect.spec.ts launchServer should respect selectors [timeout]
library/browsertype-connect.spec.ts launchServer should save download [timeout]
library/browsertype-connect.spec.ts launchServer should save har [timeout]
library/browsertype-connect.spec.ts launchServer should saveAs videos from remote browser [timeout] library/browsertype-connect.spec.ts launchServer should saveAs videos from remote browser [timeout]
library/browsertype-connect.spec.ts launchServer should set the browser connected state [timeout]
library/browsertype-connect.spec.ts launchServer should support slowmo option [timeout]
library/browsertype-connect.spec.ts launchServer should terminate network waiters [timeout]
library/browsertype-connect.spec.ts launchServer should throw when calling waitForNavigation after disconnect [timeout]
library/browsertype-connect.spec.ts launchServer should throw when used after isConnected returns false [timeout]
library/browsertype-connect.spec.ts launchServer should upload large file [timeout]
library/channels.spec.ts should work with the domain module [timeout] library/channels.spec.ts should work with the domain module [timeout]
library/chromium/chromium.spec.ts PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 serviceWorker(), and fromServiceWorker() work [timeout] library/chromium/chromium.spec.ts PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 serviceWorker(), and fromServiceWorker() work [timeout]
library/chromium/chromium.spec.ts PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 setExtraHTTPHeaders [timeout] library/chromium/chromium.spec.ts PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 setExtraHTTPHeaders [timeout]

View file

@ -21,12 +21,12 @@ it('should pass firefox user preferences', async ({ browserType, mode }) => {
const browser = await browserType.launch({ const browser = await browserType.launch({
firefoxUserPrefs: { firefoxUserPrefs: {
'network.proxy.type': 1, 'network.proxy.type': 1,
'network.proxy.http': '127.0.0.1', 'network.proxy.ssl': '127.0.0.1',
'network.proxy.http_port': 3333, 'network.proxy.ssl_port': 3333,
} }
}); });
const page = await browser.newPage(); const page = await browser.newPage();
const error = await page.goto('http://example.com').catch(e => e); const error = await page.goto('https://example.com').catch(e => e);
expect(error.message).toContain('NS_ERROR_PROXY_CONNECTION_REFUSED'); expect(error.message).toContain('NS_ERROR_PROXY_CONNECTION_REFUSED');
await browser.close(); await browser.close();
}); });
@ -36,10 +36,10 @@ it('should pass firefox user preferences in persistent', async ({ mode, launchPe
const { page } = await launchPersistent({ const { page } = await launchPersistent({
firefoxUserPrefs: { firefoxUserPrefs: {
'network.proxy.type': 1, 'network.proxy.type': 1,
'network.proxy.http': '127.0.0.1', 'network.proxy.ssl': '127.0.0.1',
'network.proxy.http_port': 3333, 'network.proxy.ssl_port': 3333,
} }
}); });
const error = await page.goto('http://example.com').catch(e => e); const error = await page.goto('https://example.com').catch(e => e);
expect(error.message).toContain('NS_ERROR_PROXY_CONNECTION_REFUSED'); expect(error.message).toContain('NS_ERROR_PROXY_CONNECTION_REFUSED');
}); });

View file

@ -459,8 +459,13 @@ await page1.GotoAsync("about:blank?foo");`);
const cli = runCLI([`--save-storage=${storageFileName}`, `--save-har=${harFileName}`]); const cli = runCLI([`--save-storage=${storageFileName}`, `--save-har=${harFileName}`]);
await cli.waitFor(`import { test, expect } from '@playwright/test'`); await cli.waitFor(`import { test, expect } from '@playwright/test'`);
await cli.process.kill('SIGINT'); await cli.process.kill('SIGINT');
const { exitCode } = await cli.process.exited; const { exitCode, signal } = await cli.process.exited;
expect(exitCode).toBe(130); if (exitCode !== null) {
expect(exitCode).toBe(130);
} else {
// If the runner is slow enough, the process will be forcibly terminated by the signal
expect(signal).toBe('SIGINT');
}
expect(fs.existsSync(storageFileName)).toBeTruthy(); expect(fs.existsSync(storageFileName)).toBeTruthy();
expect(fs.existsSync(harFileName)).toBeTruthy(); expect(fs.existsSync(harFileName)).toBeTruthy();
}); });

View file

@ -1676,7 +1676,8 @@ test('should show only one pointer with multilevel iframes', async ({ page, runA
await expect.soft(snapshotFrame.frameLocator('iframe').frameLocator('iframe').locator('x-pw-pointer')).toBeVisible(); await expect.soft(snapshotFrame.frameLocator('iframe').frameLocator('iframe').locator('x-pw-pointer')).toBeVisible();
}); });
test('should show a popover', async ({ runAndTrace, page, server }) => { test('should show a popover', async ({ runAndTrace, page, server, platform, browserName, macVersion }) => {
test.skip(platform === 'darwin' && macVersion === 13 && browserName === 'webkit', 'WebKit on macOS 13.7 reliably fails on this test for some reason');
const traceViewer = await runAndTrace(async () => { const traceViewer = await runAndTrace(async () => {
await page.setContent(` await page.setContent(`
<button popovertarget="pop">Click me</button> <button popovertarget="pop">Click me</button>

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { stripVTControlCharacters } from 'node:util';
import { stripAnsi } from '../config/utils'; import { stripAnsi } from '../config/utils';
import { test, expect } from './pageTest'; import { test, expect } from './pageTest';
@ -240,10 +241,45 @@ test.describe('toHaveURL', () => {
await expect(page).toHaveURL('data:text/html,<div>A</div>'); await expect(page).toHaveURL('data:text/html,<div>A</div>');
}); });
test('fail', async ({ page }) => { test('fail string', async ({ page }) => {
await page.goto('data:text/html,<div>B</div>'); await page.goto('data:text/html,<div>A</div>');
const error = await expect(page).toHaveURL('wrong', { timeout: 1000 }).catch(e => e); const error = await expect(page).toHaveURL('wrong', { timeout: 1000 }).catch(e => e);
expect(error.message).toContain('expect.toHaveURL with timeout 1000ms'); expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(page).toHaveURL(expected)');
expect(stripVTControlCharacters(error.message)).toContain('Expected string: "wrong"\nReceived string: "data:text/html,<div>A</div>"');
});
test('fail with invalid argument', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>');
// @ts-expect-error
const error = await expect(page).toHaveURL({}).catch(e => e);
expect(stripVTControlCharacters(error.message)).toContain('expect(page).toHaveURL(expected)\n\n\n\nMatcher error: expected value must be a string, regular expression, or predicate');
expect(stripVTControlCharacters(error.message)).toContain('Expected has type: object\nExpected has value: {}');
});
test('fail with positive predicate', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>');
const error = await expect(page).toHaveURL(_url => false).catch(e => e);
expect(stripVTControlCharacters(error.message)).toContain('expect(page).toHaveURL(expected)');
expect(stripVTControlCharacters(error.message)).toContain('Expected predicate to succeed\nReceived string: "data:text/html,<div>A</div>"');
});
test('fail with negative predicate', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>');
const error = await expect(page).not.toHaveURL(_url => true).catch(e => e);
expect(stripVTControlCharacters(error.message)).toContain('expect(page).not.toHaveURL(expected)');
expect(stripVTControlCharacters(error.message)).toContain('Expected predicate to fail\nReceived string: "data:text/html,<div>A</div>"');
});
test('resolve predicate on initial call', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>');
await expect(page).toHaveURL(url => url.href === 'data:text/html,<div>A</div>', { timeout: 1000 });
});
test('resolve predicate after retries', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>');
const expectPromise = expect(page).toHaveURL(url => url.href === 'data:text/html,<div>B</div>', { timeout: 1000 });
setTimeout(() => page.goto('data:text/html,<div>B</div>'), 500);
await expectPromise;
}); });
test('support ignoreCase', async ({ page }) => { test('support ignoreCase', async ({ page }) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -18,10 +18,9 @@
import { test as it, expect } from './pageTest'; import { test as it, expect } from './pageTest';
it('should check the box @smoke', async ({ page }) => { it('should check the box @smoke', async ({ page }) => {
await page.setContent(`<div class='middle selected row' id='component'></div>`); await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
const locator = page.locator('#component'); await page.check('input');
await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/); expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
await expect(locator).toHaveClass('middle selected row');
}); });
it('should not check the checked box', async ({ page }) => { it('should not check the checked box', async ({ page }) => {

View file

@ -93,18 +93,18 @@ it('should report shiftKey', async ({ page, server, browserName, platform }) =>
const codeForKey = { 'Shift': 16, 'Alt': 18, 'Control': 17 }; const codeForKey = { 'Shift': 16, 'Alt': 18, 'Control': 17 };
for (const modifierKey in codeForKey) { for (const modifierKey in codeForKey) {
await keyboard.down(modifierKey); await keyboard.down(modifierKey);
expect(await page.evaluate('getResult()')).toBe('Keydown: ' + modifierKey + ' ' + modifierKey + 'Left ' + codeForKey[modifierKey] + ' [' + modifierKey + ']'); expect(await page.evaluate('getResult()')).toBe('Keydown: ' + modifierKey + ' ' + modifierKey + 'Left LEFT [' + modifierKey + ']');
await keyboard.down('!'); await keyboard.down('!');
// Shift+! will generate a keypress // Shift+! will generate a keypress
if (modifierKey === 'Shift') if (modifierKey === 'Shift')
expect(await page.evaluate('getResult()')).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']\nKeypress: ! Digit1 33 33 [' + modifierKey + ']'); expect(await page.evaluate('getResult()')).toBe('Keydown: ! Digit1 STANDARD [' + modifierKey + ']\nKeypress: ! Digit1 STANDARD 33 [' + modifierKey + ']');
else else
expect(await page.evaluate('getResult()')).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']'); expect(await page.evaluate('getResult()')).toBe('Keydown: ! Digit1 STANDARD [' + modifierKey + ']');
await keyboard.up('!'); await keyboard.up('!');
expect(await page.evaluate('getResult()')).toBe('Keyup: ! Digit1 49 [' + modifierKey + ']'); expect(await page.evaluate('getResult()')).toBe('Keyup: ! Digit1 STANDARD [' + modifierKey + ']');
await keyboard.up(modifierKey); await keyboard.up(modifierKey);
expect(await page.evaluate('getResult()')).toBe('Keyup: ' + modifierKey + ' ' + modifierKey + 'Left ' + codeForKey[modifierKey] + ' []'); expect(await page.evaluate('getResult()')).toBe('Keyup: ' + modifierKey + ' ' + modifierKey + 'Left LEFT []');
} }
}); });
@ -112,31 +112,31 @@ it('should report multiple modifiers', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/keyboard.html'); await page.goto(server.PREFIX + '/input/keyboard.html');
const keyboard = page.keyboard; const keyboard = page.keyboard;
await keyboard.down('Control'); await keyboard.down('Control');
expect(await page.evaluate('getResult()')).toBe('Keydown: Control ControlLeft 17 [Control]'); expect(await page.evaluate('getResult()')).toBe('Keydown: Control ControlLeft LEFT [Control]');
await keyboard.down('Alt'); await keyboard.down('Alt');
expect(await page.evaluate('getResult()')).toBe('Keydown: Alt AltLeft 18 [Alt Control]'); expect(await page.evaluate('getResult()')).toBe('Keydown: Alt AltLeft LEFT [Alt Control]');
await keyboard.down(';'); await keyboard.down(';');
expect(await page.evaluate('getResult()')).toBe('Keydown: ; Semicolon 186 [Alt Control]'); expect(await page.evaluate('getResult()')).toBe('Keydown: ; Semicolon STANDARD [Alt Control]');
await keyboard.up(';'); await keyboard.up(';');
expect(await page.evaluate('getResult()')).toBe('Keyup: ; Semicolon 186 [Alt Control]'); expect(await page.evaluate('getResult()')).toBe('Keyup: ; Semicolon STANDARD [Alt Control]');
await keyboard.up('Control'); await keyboard.up('Control');
expect(await page.evaluate('getResult()')).toBe('Keyup: Control ControlLeft 17 [Alt]'); expect(await page.evaluate('getResult()')).toBe('Keyup: Control ControlLeft LEFT [Alt]');
await keyboard.up('Alt'); await keyboard.up('Alt');
expect(await page.evaluate('getResult()')).toBe('Keyup: Alt AltLeft 18 []'); expect(await page.evaluate('getResult()')).toBe('Keyup: Alt AltLeft LEFT []');
}); });
it('should send proper codes while typing', async ({ page, server }) => { it('should send proper codes while typing', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/keyboard.html'); await page.goto(server.PREFIX + '/input/keyboard.html');
await page.keyboard.type('!'); await page.keyboard.type('!');
expect(await page.evaluate('getResult()')).toBe( expect(await page.evaluate('getResult()')).toBe(
['Keydown: ! Digit1 49 []', ['Keydown: ! Digit1 STANDARD []',
'Keypress: ! Digit1 33 33 []', 'Keypress: ! Digit1 STANDARD 33 []',
'Keyup: ! Digit1 49 []'].join('\n')); 'Keyup: ! Digit1 STANDARD []'].join('\n'));
await page.keyboard.type('^'); await page.keyboard.type('^');
expect(await page.evaluate('getResult()')).toBe( expect(await page.evaluate('getResult()')).toBe(
['Keydown: ^ Digit6 54 []', ['Keydown: ^ Digit6 STANDARD []',
'Keypress: ^ Digit6 94 94 []', 'Keypress: ^ Digit6 STANDARD 94 []',
'Keyup: ^ Digit6 54 []'].join('\n')); 'Keyup: ^ Digit6 STANDARD []'].join('\n'));
}); });
it('should send proper codes while typing with shift', async ({ page, server }) => { it('should send proper codes while typing with shift', async ({ page, server }) => {
@ -145,10 +145,10 @@ it('should send proper codes while typing with shift', async ({ page, server })
await keyboard.down('Shift'); await keyboard.down('Shift');
await page.keyboard.type('~'); await page.keyboard.type('~');
expect(await page.evaluate('getResult()')).toBe( expect(await page.evaluate('getResult()')).toBe(
['Keydown: Shift ShiftLeft 16 [Shift]', ['Keydown: Shift ShiftLeft LEFT [Shift]',
'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode 'Keydown: ~ Backquote STANDARD [Shift]',
'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode 'Keypress: ~ Backquote STANDARD 126 [Shift]',
'Keyup: ~ Backquote 192 [Shift]'].join('\n')); 'Keyup: ~ Backquote STANDARD [Shift]'].join('\n'));
await keyboard.up('Shift'); await keyboard.up('Shift');
}); });
@ -173,54 +173,54 @@ it('should press plus', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/keyboard.html'); await page.goto(server.PREFIX + '/input/keyboard.html');
await page.keyboard.press('+'); await page.keyboard.press('+');
expect(await page.evaluate('getResult()')).toBe( expect(await page.evaluate('getResult()')).toBe(
['Keydown: + Equal 187 []', // 192 is ` keyCode ['Keydown: + Equal STANDARD []',
'Keypress: + Equal 43 43 []', // 126 is ~ charCode 'Keypress: + Equal STANDARD 43 []',
'Keyup: + Equal 187 []'].join('\n')); 'Keyup: + Equal STANDARD []'].join('\n'));
}); });
it('should press shift plus', async ({ page, server }) => { it('should press shift plus', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/keyboard.html'); await page.goto(server.PREFIX + '/input/keyboard.html');
await page.keyboard.press('Shift++'); await page.keyboard.press('Shift++');
expect(await page.evaluate('getResult()')).toBe( expect(await page.evaluate('getResult()')).toBe(
['Keydown: Shift ShiftLeft 16 [Shift]', ['Keydown: Shift ShiftLeft LEFT [Shift]',
'Keydown: + Equal 187 [Shift]', // 192 is ` keyCode 'Keydown: + Equal STANDARD [Shift]',
'Keypress: + Equal 43 43 [Shift]', // 126 is ~ charCode 'Keypress: + Equal STANDARD 43 [Shift]',
'Keyup: + Equal 187 [Shift]', 'Keyup: + Equal STANDARD [Shift]',
'Keyup: Shift ShiftLeft 16 []'].join('\n')); 'Keyup: Shift ShiftLeft LEFT []'].join('\n'));
}); });
it('should support plus-separated modifiers', async ({ page, server }) => { it('should support plus-separated modifiers', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/keyboard.html'); await page.goto(server.PREFIX + '/input/keyboard.html');
await page.keyboard.press('Shift+~'); await page.keyboard.press('Shift+~');
expect(await page.evaluate('getResult()')).toBe( expect(await page.evaluate('getResult()')).toBe(
['Keydown: Shift ShiftLeft 16 [Shift]', ['Keydown: Shift ShiftLeft LEFT [Shift]',
'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode 'Keydown: ~ Backquote STANDARD [Shift]',
'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode 'Keypress: ~ Backquote STANDARD 126 [Shift]',
'Keyup: ~ Backquote 192 [Shift]', 'Keyup: ~ Backquote STANDARD [Shift]',
'Keyup: Shift ShiftLeft 16 []'].join('\n')); 'Keyup: Shift ShiftLeft LEFT []'].join('\n'));
}); });
it('should support multiple plus-separated modifiers', async ({ page, server }) => { it('should support multiple plus-separated modifiers', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/keyboard.html'); await page.goto(server.PREFIX + '/input/keyboard.html');
await page.keyboard.press('Control+Shift+~'); await page.keyboard.press('Control+Shift+~');
expect(await page.evaluate('getResult()')).toBe( expect(await page.evaluate('getResult()')).toBe(
['Keydown: Control ControlLeft 17 [Control]', ['Keydown: Control ControlLeft LEFT [Control]',
'Keydown: Shift ShiftLeft 16 [Control Shift]', 'Keydown: Shift ShiftLeft LEFT [Control Shift]',
'Keydown: ~ Backquote 192 [Control Shift]', // 192 is ` keyCode 'Keydown: ~ Backquote STANDARD [Control Shift]',
'Keyup: ~ Backquote 192 [Control Shift]', 'Keyup: ~ Backquote STANDARD [Control Shift]',
'Keyup: Shift ShiftLeft 16 [Control]', 'Keyup: Shift ShiftLeft LEFT [Control]',
'Keyup: Control ControlLeft 17 []'].join('\n')); 'Keyup: Control ControlLeft LEFT []'].join('\n'));
}); });
it('should shift raw codes', async ({ page, server }) => { it('should shift raw codes', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/keyboard.html'); await page.goto(server.PREFIX + '/input/keyboard.html');
await page.keyboard.press('Shift+Digit3'); await page.keyboard.press('Shift+Digit3');
expect(await page.evaluate('getResult()')).toBe( expect(await page.evaluate('getResult()')).toBe(
['Keydown: Shift ShiftLeft 16 [Shift]', ['Keydown: Shift ShiftLeft LEFT [Shift]',
'Keydown: # Digit3 51 [Shift]', // 51 is # keyCode 'Keydown: # Digit3 STANDARD [Shift]',
'Keypress: # Digit3 35 35 [Shift]', // 35 is # charCode 'Keypress: # Digit3 STANDARD 35 [Shift]',
'Keyup: # Digit3 51 [Shift]', 'Keyup: # Digit3 STANDARD [Shift]',
'Keyup: Shift ShiftLeft 16 []'].join('\n')); 'Keyup: Shift ShiftLeft LEFT []'].join('\n'));
}); });
it('should specify repeat property', async ({ page, server }) => { it('should specify repeat property', async ({ page, server }) => {
@ -710,7 +710,7 @@ it('should have correct Keydown/Keyup order when pressing Escape key', async ({
await page.goto(server.PREFIX + '/input/keyboard.html'); await page.goto(server.PREFIX + '/input/keyboard.html');
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
expect(await page.evaluate('getResult()')).toBe(` expect(await page.evaluate('getResult()')).toBe(`
Keydown: Escape Escape 27 [] Keydown: Escape Escape STANDARD []
Keyup: Escape Escape 27 [] Keyup: Escape Escape STANDARD []
`.trim()); `.trim());
}); });

View file

@ -152,3 +152,60 @@ test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-2.yml'), 'utf8'); const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]'); expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
}); });
for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
test(`should update snapshot with the update-snapshots=${updateSnapshots} (config)`, async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
updateSnapshots: '${updateSnapshots}',
};
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>New content</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
});
`,
'__snapshots__/a.spec.ts/test-1.yml': '- heading "Old content" [level=1]',
});
const rebase = updateSnapshots === 'all' || updateSnapshots === 'changed';
expect(result.exitCode).toBe(rebase ? 0 : 1);
if (rebase) {
const snapshotOutputPath = testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml');
expect(result.output).toContain(`A snapshot is generated at`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe('- heading "New content" [level=1]');
} else {
expect(result.output).toContain(`expect.toMatchAriaSnapshot`);
}
});
}
test('should respect timeout', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'test.yml': `
- heading "hello world"
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
import path from 'path';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
});
`,
'__snapshots__/a.spec.ts/test-1.yml': '- heading "new world" [level=1]',
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Timed out 1ms waiting for`);
});

View file

@ -543,11 +543,31 @@ test('should respect expect.timeout', async ({ runInlineTest }) => {
'playwright.config.js': `module.exports = { expect: { timeout: 1000 } }`, 'playwright.config.js': `module.exports = { expect: { timeout: 1000 } }`,
'a.test.ts': ` 'a.test.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { stripVTControlCharacters } from 'node:util';
test('timeout', async ({ page }) => { test('timeout', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>'); await page.goto('data:text/html,<div>A</div>');
const error = await expect(page).toHaveURL('data:text/html,<div>B</div>').catch(e => e); const error = await expect(page).toHaveURL('data:text/html,<div>B</div>').catch(e => e);
expect(error.message).toContain('expect.toHaveURL with timeout 1000ms'); expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(page).toHaveURL(expected)');
expect(error.message).toContain('data:text/html,<div>');
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('should support toHaveURL predicate', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.js': `module.exports = { expect: { timeout: 1000 } }`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
import { stripVTControlCharacters } from 'node:util';
test('predicate', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>');
const error = await expect(page).toHaveURL('data:text/html,<div>B</div>').catch(e => e);
expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(page).toHaveURL(expected)');
expect(error.message).toContain('data:text/html,<div>'); expect(error.message).toContain('data:text/html,<div>');
}); });
`, `,

View file

@ -341,6 +341,36 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
expect(data.toString()).toBe(ACTUAL_SNAPSHOT); expect(data.toString()).toBe(ACTUAL_SNAPSHOT);
}); });
for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
test(`should update snapshot with the update-snapshots=${updateSnapshots} (config)`, async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `export default { updateSnapshots: '${updateSnapshots}' };`,
...files,
'a.spec.js-snapshots/snapshot.txt': 'Hello world',
'a.spec.js': `
const { test, expect } = require('./helper');
test('is a test', ({}) => {
expect('Hello world updated').toMatchSnapshot('snapshot.txt');
});
`
});
const rebase = updateSnapshots === 'all' || updateSnapshots === 'changed';
expect(result.exitCode).toBe(rebase ? 0 : 1);
if (rebase) {
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
if (updateSnapshots === 'all')
expect(result.output).toContain(`${snapshotOutputPath} is not the same, writing actual.`);
if (updateSnapshots === 'changed')
expect(result.output).toContain(`${snapshotOutputPath} does not match, writing actual.`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe('Hello world updated');
} else {
expect(result.output).toContain(`toMatchSnapshot`);
}
});
}
test('should ignore text snapshot with the ignore-snapshots flag', async ({ runInlineTest }, testInfo) => { test('should ignore text snapshot with the ignore-snapshots flag', async ({ runInlineTest }, testInfo) => {
const EXPECTED_SNAPSHOT = 'Hello world'; const EXPECTED_SNAPSHOT = 'Hello world';
const ACTUAL_SNAPSHOT = 'Hello world updated'; const ACTUAL_SNAPSHOT = 'Hello world updated';
@ -1140,3 +1170,25 @@ test('should throw if a Promise was passed to toMatchSnapshot', async ({ runInli
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
}); });
test('should respect update snapshot option from config', async ({ runInlineTest }, testInfo) => {
const EXPECTED_SNAPSHOT = 'Hello world';
const ACTUAL_SNAPSHOT = 'Hello world updated';
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT,
'a.spec.js': `
const { test, expect } = require('./helper');
test('is a test', ({}) => {
expect('${ACTUAL_SNAPSHOT}').toMatchSnapshot('snapshot.txt');
});
`
}, { 'update-snapshots': true });
expect(result.exitCode).toBe(0);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
expect(result.output).toContain(`${snapshotOutputPath} does not match, writing actual.`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe(ACTUAL_SNAPSHOT);
});

View file

@ -229,6 +229,7 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
// END: Reserved CI // END: Reserved CI
PW_TEST_HTML_REPORT_OPEN: undefined, PW_TEST_HTML_REPORT_OPEN: undefined,
PLAYWRIGHT_HTML_OPEN: undefined, PLAYWRIGHT_HTML_OPEN: undefined,
PW_TEST_DEBUG_REPORTERS: undefined,
PW_TEST_REPORTER: undefined, PW_TEST_REPORTER: undefined,
PW_TEST_REPORTER_WS_ENDPOINT: undefined, PW_TEST_REPORTER_WS_ENDPOINT: undefined,
PW_TEST_SOURCE_TRANSFORM: undefined, PW_TEST_SOURCE_TRANSFORM: undefined,

View file

@ -258,6 +258,51 @@ for (const useIntermediateMergeReport of [false, true] as const) {
expect(text).toContain('1) a.test.ts:3:15 passes outer 1.0 inner 1.1 ──'); expect(text).toContain('1) a.test.ts:3:15 passes outer 1.0 inner 1.1 ──');
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
}); });
test('print stdio', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', async ({}) => {
await new Promise(resolve => process.stdout.write('line1', () => resolve()));
await new Promise(resolve => process.stdout.write('line2\\n', () => resolve()));
await new Promise(resolve => process.stderr.write(Buffer.from(''), () => resolve()));
});
test('passes 2', async ({}) => {
await new Promise(resolve => process.stdout.write('partial', () => resolve()));
});
test('passes 3', async ({}) => {
await new Promise(resolve => process.stdout.write('full\\n', () => resolve()));
});
test('passes 4', async ({}) => {
});
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(4);
const expected = [
'#0 : 1 a.test.ts:3:15 passes',
'line1line2',
`#0 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:15 passes`,
'',
'#3 : 2 a.test.ts:9:15 passes 2',
`partial#3 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:9:15 passes 2`,
'',
'#5 : 3 a.test.ts:13:15 passes 3',
'full',
`#5 : ${POSITIVE_STATUS_MARK} 3 a.test.ts:13:15 passes 3`,
'#7 : 4 a.test.ts:17:15 passes 4',
`#7 : ${POSITIVE_STATUS_MARK} 4 a.test.ts:17:15 passes 4`,
];
const lines = result.output.split('\n');
const firstIndex = lines.indexOf(expected[0]);
expect(firstIndex, 'first line should be there').not.toBe(-1);
for (let i = 0; i < expected.length; ++i)
expect(lines[firstIndex + i]).toContain(expected[i]);
});
}); });
} }

View file

@ -5,16 +5,16 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@playwright/test": "1.50.0-alpha-2025-01-17" "@playwright/test": "1.50.0-beta-1737557690000"
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.50.0-alpha-2025-01-17", "version": "1.50.0-beta-1737557690000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-alpha-2025-01-17.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-beta-1737557690000.tgz",
"integrity": "sha512-fMUwMcP0YE2knged9GJXqv3fpT2xoywTtqYaSzpZmjnNESF+CUUAGY2hHm9/fz/v9ijcjyd62hYFbqS5KeKuHQ==", "integrity": "sha512-p6iaLrgsPatz9WqQMtxQyE9kElq8+Ae/N5i1+UF6+vxQdGpGSppk4+V4TttL8iMJhxvO/W3o/0vX8viDOVk1tg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.50.0-alpha-2025-01-17" "playwright": "1.50.0-beta-1737557690000"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -38,12 +38,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.50.0-alpha-2025-01-17", "version": "1.50.0-beta-1737557690000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-alpha-2025-01-17.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-beta-1737557690000.tgz",
"integrity": "sha512-LRavQ9Qu27nHvJ57f+7UDBTAEWhGKV+MS2qLAJpF8HXtfSMVlLK82W9Oba41lCNUzgLoAuFv0wCO/RcHqLz7yQ==", "integrity": "sha512-cgRCY5Gw0qZeqtSwvjMVVzUPQ19xLC6Z6i2oGa2Su2b4CO9qkceace1+Qe98RuCeU5nCHXbNTsPU6gue+yxagg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.50.0-alpha-2025-01-17" "playwright-core": "1.50.0-beta-1737557690000"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -56,9 +56,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.50.0-alpha-2025-01-17", "version": "1.50.0-beta-1737557690000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-alpha-2025-01-17.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-beta-1737557690000.tgz",
"integrity": "sha512-XkoLZ+7J5ybDq68xSlofPziH1Y8It9LpMisxtBfebjKWbVY8BzctlB1Da9udKDP0oWQPNq4tUnwW0hkeET3lUg==", "integrity": "sha512-pr4ZXZmn+hhFrWEIoPQZgxxwkFLPVhMH3uB5eL+SPUKLtFv0WhOoo1PHUUqinMumj84bhhYS6ODg2pPPGPG7sA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@ -70,11 +70,11 @@
}, },
"dependencies": { "dependencies": {
"@playwright/test": { "@playwright/test": {
"version": "1.50.0-alpha-2025-01-17", "version": "1.50.0-beta-1737557690000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-alpha-2025-01-17.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-beta-1737557690000.tgz",
"integrity": "sha512-fMUwMcP0YE2knged9GJXqv3fpT2xoywTtqYaSzpZmjnNESF+CUUAGY2hHm9/fz/v9ijcjyd62hYFbqS5KeKuHQ==", "integrity": "sha512-p6iaLrgsPatz9WqQMtxQyE9kElq8+Ae/N5i1+UF6+vxQdGpGSppk4+V4TttL8iMJhxvO/W3o/0vX8viDOVk1tg==",
"requires": { "requires": {
"playwright": "1.50.0-alpha-2025-01-17" "playwright": "1.50.0-beta-1737557690000"
} }
}, },
"fsevents": { "fsevents": {
@ -84,18 +84,18 @@
"optional": true "optional": true
}, },
"playwright": { "playwright": {
"version": "1.50.0-alpha-2025-01-17", "version": "1.50.0-beta-1737557690000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-alpha-2025-01-17.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-beta-1737557690000.tgz",
"integrity": "sha512-LRavQ9Qu27nHvJ57f+7UDBTAEWhGKV+MS2qLAJpF8HXtfSMVlLK82W9Oba41lCNUzgLoAuFv0wCO/RcHqLz7yQ==", "integrity": "sha512-cgRCY5Gw0qZeqtSwvjMVVzUPQ19xLC6Z6i2oGa2Su2b4CO9qkceace1+Qe98RuCeU5nCHXbNTsPU6gue+yxagg==",
"requires": { "requires": {
"fsevents": "2.3.2", "fsevents": "2.3.2",
"playwright-core": "1.50.0-alpha-2025-01-17" "playwright-core": "1.50.0-beta-1737557690000"
} }
}, },
"playwright-core": { "playwright-core": {
"version": "1.50.0-alpha-2025-01-17", "version": "1.50.0-beta-1737557690000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-alpha-2025-01-17.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-beta-1737557690000.tgz",
"integrity": "sha512-XkoLZ+7J5ybDq68xSlofPziH1Y8It9LpMisxtBfebjKWbVY8BzctlB1Da9udKDP0oWQPNq4tUnwW0hkeET3lUg==" "integrity": "sha512-pr4ZXZmn+hhFrWEIoPQZgxxwkFLPVhMH3uB5eL+SPUKLtFv0WhOoo1PHUUqinMumj84bhhYS6ODg2pPPGPG7sA=="
} }
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"@playwright/test": "1.50.0-alpha-2025-01-17" "@playwright/test": "1.50.0-beta-1737557690000"
} }
} }

View file

@ -398,7 +398,7 @@ test('should show custom fixture titles in actions tree', async ({ runUITest })
const { page } = await runUITest({ const { page } = await runUITest({
'a.test.ts': ` 'a.test.ts': `
import { test as base, expect } from '@playwright/test'; import { test as base, expect } from '@playwright/test';
const test = base.extend({ const test = base.extend({
fixture1: [async ({}, use) => { fixture1: [async ({}, use) => {
await use(); await use();
@ -457,7 +457,7 @@ test('attachments tab shows all but top-level .push attachments', async ({ runUI
- tree: - tree:
- treeitem /step/: - treeitem /step/:
- group: - group:
- treeitem /attach \\"foo-attach\\"/ - treeitem /attach \\"foo-attach\\"/
- treeitem /attach \\"bar-push\\"/ - treeitem /attach \\"bar-push\\"/
- treeitem /attach \\"bar-attach\\"/ - treeitem /attach \\"bar-attach\\"/
`); `);
@ -470,3 +470,32 @@ test('attachments tab shows all but top-level .push attachments', async ({ runUI
- button /bar-attach/ - button /bar-attach/
`); `);
}); });
test('skipped steps should have an indicator', async ({ runUITest }) => {
const { page } = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('test with steps', async ({}) => {
await test.step('outer', async () => {
await test.step.skip('skipped1', () => {});
});
await test.step.skip('skipped2', () => {});
});
`,
});
await page.getByRole('treeitem', { name: 'test with steps' }).dblclick();
const actionsTree = page.getByTestId('actions-tree');
await actionsTree.getByRole('treeitem', { name: 'outer' }).click();
await page.keyboard.press('ArrowRight');
await expect(actionsTree).toMatchAriaSnapshot(`
- tree:
- treeitem /outer/ [expanded]:
- group:
- treeitem /skipped1/
- treeitem /skipped2/
`);
const skippedMarker = actionsTree.getByRole('treeitem', { name: 'skipped1' }).locator('.action-skipped');
await expect(skippedMarker).toBeVisible();
await expect(skippedMarker).toHaveAccessibleName('skipped');
});

View file

@ -454,6 +454,50 @@ test('should generate baseline for input values', async ({ runInlineTest }, test
expect(result2.exitCode).toBe(0); expect(result2.exitCode).toBe(0);
}); });
test('should update when options are specified', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'.git/marker': '',
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<input value="hello world">\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\`\`, { timeout: 2500 });
await expect(page.locator('body')).toMatchAriaSnapshot('',
{
timeout: 2500
});
});
`
});
expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
--- a/a.spec.ts
+++ b/a.spec.ts
@@ -2,8 +2,12 @@
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<input value="hello world">\`);
- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`, { timeout: 2500 });
- await expect(page.locator('body')).toMatchAriaSnapshot('',
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
+ - textbox: hello world
+ \`, { timeout: 2500 });
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
+ - textbox: hello world
+ \`,
{
timeout: 2500
});
`);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
const result2 = await runInlineTest({});
expect(result2.exitCode).toBe(0);
});
test('should not update snapshots when locator did not match', async ({ runInlineTest }, testInfo) => { test('should not update snapshots when locator did not match', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
'.git/marker': '', '.git/marker': '',
@ -617,4 +661,45 @@ test.describe('update-source-method', () => {
a.spec.ts a.spec.ts
`); `);
}); });
test('should overwrite source when specified in the config', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'.git/marker': '',
'playwright.config.ts': `
export default { updateSourceMethod: 'overwrite' };
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\`
- heading "world"
\`);
});
`
}, { 'update-snapshots': 'all' });
expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
expect(fs.existsSync(patchPath)).toBeFalsy();
const data = fs.readFileSync(testInfo.outputPath('a.spec.ts'), 'utf-8');
expect(data).toBe(`
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\`
- heading "hello" [level=1]
\`);
});
`);
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
a.spec.ts
`);
const result2 = await runInlineTest({});
expect(result2.exitCode).toBe(0);
});
}); });

View file

@ -4,7 +4,7 @@ set -x
trap "cd $(pwd -P)" EXIT trap "cd $(pwd -P)" EXIT
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
NODE_VERSION="22.13.0" # autogenerated via ./update-playwright-driver-version.mjs NODE_VERSION="22.13.1" # autogenerated via ./update-playwright-driver-version.mjs
cd "$(dirname "$0")" cd "$(dirname "$0")"
PACKAGE_VERSION=$(node -p "require('../../package.json').version") PACKAGE_VERSION=$(node -p "require('../../package.json').version")

View file

@ -5,24 +5,20 @@ The data is consumed by https://devops.playwright.dev/flakiness.html
## Publish ## Publish
Azure Functions Core Tools is not available on macOS M1 yet, so we use GitHub Codespaces to publish the function. - Install [Azure Functions Core Tools version 4](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=macos%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-javascript):
### Via GitHub Codespaces:
- Install [Azure Functions Core Tools version 4](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=linux%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-javascript):
``` ```
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg brew tap azure/functions
mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg brew install azure-functions-core-tools@4
sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' # if upgrading on a machine that has 2.x or 3.x installed:
apt-get update && apt-get install azure-functions-core-tools-4 sudo brew link --overwrite azure-functions-core-tools@4
``` ```
- Install Azure CLI: - Install Azure CLI:
```bash ```bash
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash brew update && brew install azure-cli
``` ```
- Login to Azure: - Login to Azure CLI and select the subscription (popup will open):
```bash ```bash
az login --use-device-code az login
``` ```
- Install NPM Deps (`node_modules/` folder will be published as-is): - Install NPM Deps (`node_modules/` folder will be published as-is):
``` ```