Merge branch 'main' into mockingproxy
3
.github/workflows/tests_bidi.yml
vendored
|
|
@ -7,7 +7,7 @@ on:
|
|||
- main
|
||||
paths:
|
||||
- .github/workflows/tests_bidi.yml
|
||||
- packages/playwright-core/src/server/bidi/*
|
||||
- packages/playwright-core/src/server/bidi/**
|
||||
schedule:
|
||||
# Run every day at midnight
|
||||
- cron: '0 0 * * *'
|
||||
|
|
@ -18,6 +18,7 @@ env:
|
|||
jobs:
|
||||
test_bidi:
|
||||
name: BiDi
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
|
|
|
|||
1
.github/workflows/tests_components.yml
vendored
|
|
@ -20,6 +20,7 @@ env:
|
|||
jobs:
|
||||
test_components:
|
||||
name: ${{ matrix.os }} - Node.js ${{ matrix.node-version }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
|
|||
5
.github/workflows/tests_others.yml
vendored
|
|
@ -21,6 +21,7 @@ env:
|
|||
jobs:
|
||||
test_stress:
|
||||
name: Stress - ${{ matrix.os }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -57,6 +58,7 @@ jobs:
|
|||
|
||||
test_webview2:
|
||||
name: WebView2
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: windows-2022
|
||||
permissions:
|
||||
|
|
@ -87,6 +89,7 @@ jobs:
|
|||
|
||||
test_clock_frozen_time_linux:
|
||||
name: time library - ${{ matrix.clock }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
|
|
@ -112,6 +115,7 @@ jobs:
|
|||
|
||||
test_clock_frozen_time_test_runner:
|
||||
name: time test runner - ${{ matrix.clock }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
|
|
@ -136,6 +140,7 @@ jobs:
|
|||
|
||||
test_electron:
|
||||
name: Electron - ${{ matrix.os }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
6
.github/workflows/tests_primary.yml
vendored
|
|
@ -27,6 +27,7 @@ env:
|
|||
jobs:
|
||||
test_linux:
|
||||
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 }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -59,6 +60,7 @@ jobs:
|
|||
|
||||
test_linux_chromium_tot:
|
||||
name: ${{ matrix.os }} (chromium tip-of-tree)
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -83,6 +85,7 @@ jobs:
|
|||
|
||||
test_test_runner:
|
||||
name: Test Runner
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -127,6 +130,7 @@ jobs:
|
|||
|
||||
test_web_components:
|
||||
name: Web Components
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -162,6 +166,7 @@ jobs:
|
|||
|
||||
test_vscode_extension:
|
||||
name: VSCode Extension
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PWTEST_BOT_NAME: "vscode-extension"
|
||||
|
|
@ -198,6 +203,7 @@ jobs:
|
|||
|
||||
test_package_installations:
|
||||
name: "Installation Test ${{ matrix.os }}"
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
13
.github/workflows/tests_secondary.yml
vendored
|
|
@ -26,6 +26,7 @@ permissions:
|
|||
jobs:
|
||||
test_linux:
|
||||
name: ${{ matrix.os }} (${{ matrix.browser }})
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -46,6 +47,7 @@ jobs:
|
|||
|
||||
test_mac:
|
||||
name: ${{ matrix.os }} (${{ matrix.browser }})
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -73,6 +75,7 @@ jobs:
|
|||
|
||||
test_win:
|
||||
name: "Windows"
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -92,6 +95,7 @@ jobs:
|
|||
|
||||
test-package-installations-other-node-versions:
|
||||
name: "Installation Test ${{ matrix.os }} (${{ matrix.node_version }})"
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
|
@ -125,6 +129,7 @@ jobs:
|
|||
|
||||
headed_tests:
|
||||
name: "headed ${{ matrix.browser }} (${{ matrix.os }})"
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -151,6 +156,7 @@ jobs:
|
|||
|
||||
transport_linux:
|
||||
name: "Transport"
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -172,6 +178,7 @@ jobs:
|
|||
|
||||
tracing_linux:
|
||||
name: Tracing ${{ matrix.browser }} ${{ matrix.channel }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -199,6 +206,7 @@ jobs:
|
|||
|
||||
test_chromium_channels:
|
||||
name: Test ${{ matrix.channel }} on ${{ matrix.runs-on }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
strategy:
|
||||
|
|
@ -221,6 +229,7 @@ jobs:
|
|||
|
||||
chromium_tot:
|
||||
name: Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
|
@ -243,6 +252,7 @@ jobs:
|
|||
|
||||
chromium_tot_headless_shell:
|
||||
name: Chromium tip-of-tree headless-shell-${{ matrix.os }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
|
@ -264,6 +274,7 @@ jobs:
|
|||
|
||||
firefox_beta:
|
||||
name: Firefox Beta ${{ matrix.os }}
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
|
@ -285,6 +296,7 @@ jobs:
|
|||
|
||||
build-playwright-driver:
|
||||
name: "build-playwright-driver"
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -298,6 +310,7 @@ jobs:
|
|||
|
||||
test_channel_chromium:
|
||||
name: Test channel=chromium
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
1
.github/workflows/tests_service.yml
vendored
|
|
@ -10,6 +10,7 @@ env:
|
|||
jobs:
|
||||
test:
|
||||
name: "Service"
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
|
|||
1
.github/workflows/tests_video.yml
vendored
|
|
@ -14,6 +14,7 @@ env:
|
|||
jobs:
|
||||
video_linux:
|
||||
name: "Video Linux"
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: allow-uploading-flakiness-results
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
1
.github/workflows/trigger_tests.yml
vendored
|
|
@ -9,6 +9,7 @@ on:
|
|||
jobs:
|
||||
trigger:
|
||||
name: "trigger"
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- run: |
|
||||
|
|
|
|||
2
.gitignore
vendored
|
|
@ -35,4 +35,4 @@ test-results
|
|||
.cache/
|
||||
.eslintcache
|
||||
playwright.env
|
||||
firefox
|
||||
/firefox/
|
||||
|
|
|
|||
35
FILING_ISSUES.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# How to File a Bug Report That Actually Gets Resolved
|
||||
|
||||
Make sure you’re 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 can’t reproduce them ourselves.
|
||||
- We can’t 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 Overflow’s 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 it’s unlikely we can assist.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | 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: |
|
||||
| Firefox <!-- GEN:firefox-version -->134.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
REMOTE_URL="https://github.com/mozilla/gecko-dev"
|
||||
BASE_BRANCH="release"
|
||||
BASE_REVISION="bc78b98043438d8ee2727a483b6e10dedfda883f"
|
||||
BASE_REVISION="5cfa81898f6eef8fb1abe463e5253cea5bc17f3f"
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ class PageTarget {
|
|||
this._videoRecordingInfo = undefined;
|
||||
this._screencastRecordingInfo = undefined;
|
||||
this._dialogs = new Map();
|
||||
this.forcedColors = 'no-override';
|
||||
this.forcedColors = 'none';
|
||||
this.disableCache = false;
|
||||
this.mediumOverride = '';
|
||||
this.crossProcessCookie = {
|
||||
|
|
@ -635,7 +635,8 @@ class PageTarget {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -858,8 +859,8 @@ function fromProtocolReducedMotion(reducedMotion) {
|
|||
function fromProtocolForcedColors(forcedColors) {
|
||||
if (forcedColors === 'active' || forcedColors === 'none')
|
||||
return forcedColors;
|
||||
if (forcedColors === null)
|
||||
return undefined;
|
||||
if (!forcedColors)
|
||||
return 'none';
|
||||
throw new Error('Unknown forced colors: ' + forcedColors);
|
||||
}
|
||||
|
||||
|
|
@ -893,7 +894,7 @@ class BrowserContext {
|
|||
this.forceOffline = false;
|
||||
this.disableCache = false;
|
||||
this.colorScheme = 'none';
|
||||
this.forcedColors = 'no-override';
|
||||
this.forcedColors = 'none';
|
||||
this.reducedMotion = 'none';
|
||||
this.videoRecordingOptions = undefined;
|
||||
this.crossProcessCookie = {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,10 @@ class Juggler {
|
|||
};
|
||||
|
||||
// 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 browserHandler;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
REMOTE_URL="https://github.com/WebKit/WebKit.git"
|
||||
BASE_BRANCH="main"
|
||||
BASE_REVISION="8ceb1da47e75a488ae4c12017a861636904acd4f"
|
||||
BASE_REVISION="76c95d6131edd36775a5eac01e297926fc974be8"
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
#import <WebKit/WKUserContentControllerPrivate.h>
|
||||
#import <WebKit/WKWebViewConfigurationPrivate.h>
|
||||
#import <WebKit/WKWebViewPrivate.h>
|
||||
#import <WebKit/WKWebpagePreferencesPrivate.h>
|
||||
#import <WebKit/WKWebsiteDataStorePrivate.h>
|
||||
#import <WebKit/WebNSURLExtras.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
|
@ -240,6 +241,8 @@ const NSActivityOptions ActivityOptions =
|
|||
configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO;
|
||||
configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO;
|
||||
configuration.preferences._domTimersThrottlingEnabled = NO;
|
||||
// Do not auto play audio and video with sound.
|
||||
configuration.defaultWebpagePreferences._autoplayPolicy = _WKWebsiteAutoplayPolicyAllowWithoutSound;
|
||||
_WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease];
|
||||
processConfiguration.forceOverlayScrollbars = YES;
|
||||
configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease];
|
||||
|
|
|
|||
|
|
@ -864,31 +864,6 @@ If [`param: expression`] throws or rejects, this method throws.
|
|||
|
||||
**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-%%
|
||||
* since: v1.14
|
||||
|
||||
|
|
|
|||
|
|
@ -2250,7 +2250,6 @@ Asserts that the target element matches the given [accessibility snapshot](../ar
|
|||
```js
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
|
||||
```
|
||||
|
||||
```python async
|
||||
|
|
|
|||
|
|
@ -323,17 +323,18 @@ expect(page).to_have_url(re.compile(".*checkout"))
|
|||
await Expect(Page).ToHaveURLAsync(new Regex(".*checkout"));
|
||||
```
|
||||
|
||||
### param: PageAssertions.toHaveURL.urlOrRegExp
|
||||
### param: PageAssertions.toHaveURL.url
|
||||
* 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
|
||||
* since: v1.44
|
||||
- `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-%%
|
||||
* since: v1.18
|
||||
|
|
|
|||
|
|
@ -6,6 +6,74 @@ toc_max_heading_level: 2
|
|||
|
||||
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
|
||||
|
||||
<LiteYouTube
|
||||
|
|
|
|||
|
|
@ -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"`.
|
||||
- `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.
|
||||
- `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"]>
|
||||
- `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.
|
||||
|
|
|
|||
|
|
@ -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-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". |
|
||||
| `--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%). |
|
||||
| `-x` | Stop after the first failure. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
```js title="my-test.ts"
|
||||
import * as debug from 'debug';
|
||||
import debug from 'debug';
|
||||
import * as fs from 'fs';
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`. |
|
||||
| `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. |
|
||||
| `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
|
||||
|
||||
|
|
|
|||
18
package-lock.json
generated
|
|
@ -61,7 +61,7 @@
|
|||
"react-dom": "^18.1.0",
|
||||
"ssim.js": "^3.5.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.6",
|
||||
"vite": "^5.4.14",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yaml": "^2.6.0"
|
||||
|
|
@ -6931,10 +6931,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.28.4",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
|
||||
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
|
||||
"version": "5.28.5",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
|
||||
"integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^2.0.0"
|
||||
},
|
||||
|
|
@ -7027,9 +7028,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||
"version": "5.4.14",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
|
||||
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
|
@ -7833,7 +7835,7 @@
|
|||
"dependencies": {
|
||||
"playwright": "1.51.0-next",
|
||||
"playwright-core": "1.51.0-next",
|
||||
"vite": "^5.2.8"
|
||||
"vite": "^5.4.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@
|
|||
"react-dom": "^18.1.0",
|
||||
"ssim.js": "^3.5.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.6",
|
||||
"vite": "^5.4.14",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yaml": "^2.6.0"
|
||||
|
|
|
|||
|
|
@ -3,31 +3,31 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1155",
|
||||
"revision": "1156",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "133.0.6943.16"
|
||||
"browserVersion": "133.0.6943.27"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1293",
|
||||
"revision": "1297",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "133.0.6943.0"
|
||||
"browserVersion": "134.0.6974.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1471",
|
||||
"revision": "1472",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "134.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1467",
|
||||
"revision": "1468",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "133.0b9"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2123",
|
||||
"revision": "2125",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"debian11-x64": "2105",
|
||||
|
|
|
|||
|
|
@ -118,7 +118,14 @@ export class PlaywrightConnection {
|
|||
const playwright = createPlaywright({ sdkLanguage: options.sdkLanguage, isServer: true });
|
||||
|
||||
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 () => {
|
||||
for (const browser of playwright.allBrowsers())
|
||||
|
|
|
|||
|
|
@ -139,6 +139,9 @@ function defaultProfilePreferences(
|
|||
'dom.min_background_timeout_value_without_budget_throttling': 0,
|
||||
'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
|
||||
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
|
||||
'extensions.autoDisableScopes': 0,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@ export const chromiumSwitches = [
|
|||
// ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230
|
||||
// LensOverlay - Hides the Lens feature in the URL address bar. Its not working in unofficial builds.
|
||||
// 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',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-ipc-flooding-protection',
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy S5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 740
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 740,
|
||||
"height": 360
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 320,
|
||||
"height": 658
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+ landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 658,
|
||||
"height": 320
|
||||
|
|
@ -176,7 +176,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 712,
|
||||
"height": 1138
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1138,
|
||||
"height": 712
|
||||
|
|
@ -1098,7 +1098,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"LG Optimus L70": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/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": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1109,7 +1109,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"LG Optimus L70 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1120,7 +1120,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 550": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1131,7 +1131,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 550 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1142,7 +1142,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 950": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1153,7 +1153,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 950 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1164,7 +1164,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 800,
|
||||
"height": 1280
|
||||
|
|
@ -1175,7 +1175,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
|
|
@ -1186,7 +1186,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1197,7 +1197,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1208,7 +1208,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1219,7 +1219,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1230,7 +1230,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1241,7 +1241,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1252,7 +1252,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1263,7 +1263,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1274,7 +1274,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1285,7 +1285,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1296,7 +1296,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 600,
|
||||
"height": 960
|
||||
|
|
@ -1307,7 +1307,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 960,
|
||||
"height": 600
|
||||
|
|
@ -1362,7 +1362,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Pixel 2": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 411,
|
||||
"height": 731
|
||||
|
|
@ -1373,7 +1373,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 731,
|
||||
"height": 411
|
||||
|
|
@ -1384,7 +1384,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 411,
|
||||
"height": 823
|
||||
|
|
@ -1395,7 +1395,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 823,
|
||||
"height": 411
|
||||
|
|
@ -1406,7 +1406,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 393,
|
||||
"height": 786
|
||||
|
|
@ -1417,7 +1417,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 786,
|
||||
"height": 393
|
||||
|
|
@ -1428,7 +1428,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 353,
|
||||
"height": 745
|
||||
|
|
@ -1439,7 +1439,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 745,
|
||||
"height": 353
|
||||
|
|
@ -1450,7 +1450,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G)": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 892
|
||||
|
|
@ -1465,7 +1465,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G) landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"height": 892,
|
||||
"width": 412
|
||||
|
|
@ -1480,7 +1480,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 393,
|
||||
"height": 851
|
||||
|
|
@ -1495,7 +1495,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 851,
|
||||
"height": 393
|
||||
|
|
@ -1510,7 +1510,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 915
|
||||
|
|
@ -1525,7 +1525,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 915,
|
||||
"height": 412
|
||||
|
|
@ -1540,7 +1540,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1551,7 +1551,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1562,7 +1562,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Chrome HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1577,7 +1577,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1622,7 +1622,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Desktop Chrome": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1637,7 +1637,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -301,6 +301,11 @@ export module Protocol {
|
|||
forcedColors: ("active"|"none")|null;
|
||||
};
|
||||
export type setForcedColorsReturnValue = void;
|
||||
export type setContrastParameters = {
|
||||
browserContextId?: string;
|
||||
contrast: ("less"|"more"|"custom"|"no-preference")|null;
|
||||
};
|
||||
export type setContrastReturnValue = void;
|
||||
export type setVideoRecordingOptionsParameters = {
|
||||
browserContextId?: string;
|
||||
options?: {
|
||||
|
|
@ -530,6 +535,7 @@ export module Protocol {
|
|||
colorScheme?: ("dark"|"light"|"no-preference");
|
||||
reducedMotion?: ("reduce"|"no-preference");
|
||||
forcedColors?: ("active"|"none");
|
||||
contrast?: ("less"|"more"|"custom"|"no-preference");
|
||||
};
|
||||
export type setEmulatedMediaReturnValue = void;
|
||||
export type setCacheDisabledParameters = {
|
||||
|
|
@ -1131,6 +1137,7 @@ export module Protocol {
|
|||
"Browser.setColorScheme": Browser.setColorSchemeParameters;
|
||||
"Browser.setReducedMotion": Browser.setReducedMotionParameters;
|
||||
"Browser.setForcedColors": Browser.setForcedColorsParameters;
|
||||
"Browser.setContrast": Browser.setContrastParameters;
|
||||
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsParameters;
|
||||
"Browser.cancelDownload": Browser.cancelDownloadParameters;
|
||||
"Heap.collectGarbage": Heap.collectGarbageParameters;
|
||||
|
|
@ -1213,6 +1220,7 @@ export module Protocol {
|
|||
"Browser.setColorScheme": Browser.setColorSchemeReturnValue;
|
||||
"Browser.setReducedMotion": Browser.setReducedMotionReturnValue;
|
||||
"Browser.setForcedColors": Browser.setForcedColorsReturnValue;
|
||||
"Browser.setContrast": Browser.setContrastReturnValue;
|
||||
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsReturnValue;
|
||||
"Browser.cancelDownload": Browser.cancelDownloadReturnValue;
|
||||
"Heap.collectGarbage": Heap.collectGarbageReturnValue;
|
||||
|
|
|
|||
|
|
@ -1404,8 +1404,6 @@ export class InjectedScript {
|
|||
received = getAriaRole(element) || '';
|
||||
} else if (expression === 'to.have.title') {
|
||||
received = this.document.title;
|
||||
} else if (expression === 'to.have.url') {
|
||||
received = this.document.location.href;
|
||||
} else if (expression === 'to.have.value') {
|
||||
element = this.retarget(element, 'follow-label')!;
|
||||
if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')
|
||||
|
|
|
|||
|
|
@ -214,12 +214,6 @@ export class HttpServer {
|
|||
}
|
||||
|
||||
_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') {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
|
|
|
|||
12
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -12176,12 +12176,6 @@ export interface Locator {
|
|||
* rejects, this method throws.
|
||||
*
|
||||
* **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 arg Optional argument to pass to
|
||||
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression).
|
||||
|
|
@ -12207,12 +12201,6 @@ export interface Locator {
|
|||
* rejects, this method throws.
|
||||
*
|
||||
* **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 arg Optional argument to pass to
|
||||
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression).
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.51.0-next",
|
||||
"vite": "^5.2.8",
|
||||
"vite": "^5.4.14",
|
||||
"playwright": "1.51.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,8 +240,8 @@ function validateConfig(file: string, config: Config) {
|
|||
}
|
||||
|
||||
if ('updateSnapshots' in config && config.updateSnapshots !== undefined) {
|
||||
if (typeof config.updateSnapshots !== 'string' || !['all', 'none', 'missing'].includes(config.updateSnapshots))
|
||||
throw errorWithFile(file, `config.updateSnapshots must be one of "all", "none" or "missing"`);
|
||||
if (typeof config.updateSnapshots !== 'string' || !['all', 'changed', 'missing', 'none'].includes(config.updateSnapshots))
|
||||
throw errorWithFile(file, `config.updateSnapshots must be one of "all", "changed", "missing" or "none"`);
|
||||
}
|
||||
|
||||
if ('workers' in config && config.workers !== undefined) {
|
||||
|
|
|
|||
|
|
@ -21,11 +21,12 @@ import { expectTypes, callLogText } from '../util';
|
|||
import { toBeTruthy } from './toBeTruthy';
|
||||
import { toEqual } from './toEqual';
|
||||
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 { TestInfoImpl } from '../worker/testInfo';
|
||||
import type { ExpectMatcherState } from '../../types/test';
|
||||
import { takeFirst } from '../common/config';
|
||||
import { toHaveURL as toHaveURLExternal } from './toHaveURL';
|
||||
|
||||
export interface LocatorEx extends Locator {
|
||||
_expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
|
||||
|
|
@ -382,16 +383,10 @@ export function toHaveTitle(
|
|||
export function toHaveURL(
|
||||
this: ExpectMatcherState,
|
||||
page: Page,
|
||||
expected: string | RegExp,
|
||||
options?: { ignoreCase?: boolean, timeout?: number },
|
||||
expected: string | RegExp | ((url: URL) => boolean),
|
||||
options?: { ignoreCase?: boolean; timeout?: number },
|
||||
) {
|
||||
const baseURL = (page.context() as any)._options.baseURL;
|
||||
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);
|
||||
return toHaveURLExternal.call(this, page, expected, options);
|
||||
}
|
||||
|
||||
export async function toBeOK(
|
||||
|
|
|
|||
153
packages/playwright/src/matchers/toHaveURL.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -30,12 +30,13 @@ import path from 'path';
|
|||
type ToMatchAriaSnapshotExpected = {
|
||||
name?: string;
|
||||
path?: string;
|
||||
timeout?: number;
|
||||
} | string;
|
||||
|
||||
export async function toMatchAriaSnapshot(
|
||||
this: ExpectMatcherState,
|
||||
receiver: LocatorEx,
|
||||
expectedParam: ToMatchAriaSnapshotExpected,
|
||||
expectedParam?: ToMatchAriaSnapshotExpected,
|
||||
options: { timeout?: number } = {},
|
||||
): Promise<MatcherResult<string | RegExp, string>> {
|
||||
const matcherName = 'toMatchAriaSnapshot';
|
||||
|
|
@ -55,9 +56,11 @@ export async function toMatchAriaSnapshot(
|
|||
};
|
||||
|
||||
let expected: string;
|
||||
let timeout: number;
|
||||
let expectedPath: string | undefined;
|
||||
if (isString(expectedParam)) {
|
||||
expected = expectedParam;
|
||||
timeout = options.timeout ?? this.timeout;
|
||||
} else {
|
||||
if (expectedParam?.name) {
|
||||
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
|
||||
|
|
@ -71,6 +74,7 @@ export async function toMatchAriaSnapshot(
|
|||
expectedPath = testInfo.snapshotPath(sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml');
|
||||
}
|
||||
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
|
||||
timeout = expectedParam?.timeout ?? this.timeout;
|
||||
}
|
||||
|
||||
const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
|
||||
|
|
@ -84,7 +88,6 @@ export async function toMatchAriaSnapshot(
|
|||
}
|
||||
}
|
||||
|
||||
const timeout = options.timeout ?? this.timeout;
|
||||
expected = unshift(expected);
|
||||
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;
|
||||
|
|
@ -134,7 +137,7 @@ export async function toMatchAriaSnapshot(
|
|||
}
|
||||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
|
||||
} 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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -282,11 +282,11 @@ async function mergeReports(reportDir: string | undefined, opts: { [key: string]
|
|||
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
|
||||
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))
|
||||
updateSnapshots = options.updateSnapshots;
|
||||
else
|
||||
updateSnapshots = 'updateSnapshots' in options ? 'changed' : 'missing';
|
||||
updateSnapshots = 'updateSnapshots' in options ? 'changed' : undefined;
|
||||
|
||||
const overrides: ConfigCLIOverrides = {
|
||||
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,
|
||||
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
|
||||
updateSnapshots,
|
||||
updateSourceMethod: options.updateSourceMethod || 'patch',
|
||||
updateSourceMethod: options.updateSourceMethod,
|
||||
workers: options.workers,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ class ListReporter extends TerminalReporter {
|
|||
if (this._needNewLine) {
|
||||
this._needNewLine = false;
|
||||
process.stdout.write('\n');
|
||||
++this._lastRow;
|
||||
this._lastColumn = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,6 +212,7 @@ class ListReporter extends TerminalReporter {
|
|||
process.stdout.write('\n');
|
||||
}
|
||||
++this._lastRow;
|
||||
this._lastColumn = 0;
|
||||
}
|
||||
|
||||
private _updateLine(row: number, text: string, prefix: string) {
|
||||
|
|
|
|||
|
|
@ -68,24 +68,27 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
|
|||
traverse(fileNode, {
|
||||
CallExpression: path => {
|
||||
const node = path.node;
|
||||
if (node.arguments.length !== 1)
|
||||
if (node.arguments.length < 1)
|
||||
return;
|
||||
if (!t.isMemberExpression(node.callee))
|
||||
return;
|
||||
const argument = node.arguments[0];
|
||||
if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument))
|
||||
return;
|
||||
|
||||
const matcher = node.callee.property;
|
||||
const prop = 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) {
|
||||
// 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;
|
||||
if (matcher.loc!.start.column + 1 !== replacement.location.column)
|
||||
if (prop.loc.start.column + 1 !== replacement.location.column)
|
||||
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);
|
||||
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,
|
||||
// for example when a single test runs multiple times because of projects or retries.
|
||||
// Do not apply multiple replacements for the same assertion.
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
location: data.location,
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -421,7 +421,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
} else {
|
||||
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
|
||||
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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
type: 'before',
|
||||
callId,
|
||||
parentId,
|
||||
startTime: monotonicTime(),
|
||||
class: 'Test',
|
||||
method: 'step',
|
||||
method: category,
|
||||
apiName,
|
||||
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
|
||||
stack,
|
||||
|
|
|
|||
|
|
@ -75,16 +75,20 @@ export class WorkerMain extends ProcessRunner {
|
|||
|
||||
process.on('unhandledRejection', reason => this.unhandledError(reason));
|
||||
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._currentTest?._tracing.appendStdioToTrace('stdout', chunk);
|
||||
if (typeof cb === 'function')
|
||||
process.nextTick(cb);
|
||||
return true;
|
||||
};
|
||||
|
||||
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._currentTest?._tracing.appendStdioToTrace('stderr', chunk);
|
||||
if (typeof cb === 'function')
|
||||
process.nextTick(cb);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
18
packages/playwright/types/test.d.ts
vendored
|
|
@ -8714,7 +8714,6 @@ interface LocatorAssertions {
|
|||
* ```js
|
||||
* await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
|
||||
* await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
|
||||
* ```
|
||||
*
|
||||
* @param options
|
||||
|
|
@ -8819,14 +8818,18 @@ interface PageAssertions {
|
|||
* 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
|
||||
*/
|
||||
toHaveURL(urlOrRegExp: string|RegExp, options?: {
|
||||
toHaveURL(url: string|RegExp|((url: URL) => boolean), options?: {
|
||||
/**
|
||||
* Whether to perform case-insensitive match.
|
||||
* [`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;
|
||||
|
||||
|
|
@ -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:
|
||||
* '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.
|
||||
* '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`.
|
||||
*/
|
||||
gracefulShutdown?: {
|
||||
signal: "SIGINT"|"SIGTERM";
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@
|
|||
color: var(--vscode-editorCodeLens-foreground);
|
||||
}
|
||||
|
||||
.action-skipped {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
flex: none;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace';
|
||||
import { msToString } from '@web/uiUtils';
|
||||
import { clsx, msToString } from '@web/uiUtils';
|
||||
import * as React from 'react';
|
||||
import './actionList.css';
|
||||
import * as modelUtil from './modelUtil';
|
||||
|
|
@ -25,6 +25,7 @@ import { TreeView } from '@web/components/treeView';
|
|||
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
|
||||
import type { Boundaries } from './geometry';
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
import { testStatusIcon } from './testUtils';
|
||||
|
||||
export interface ActionListProps {
|
||||
actions: ActionTraceEventInContext[],
|
||||
|
|
@ -119,6 +120,7 @@ export const renderAction = (
|
|||
|
||||
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
|
||||
|
||||
const isSkipped = action.class === 'Test' && action.method === 'test.step.skip';
|
||||
let time: string = '';
|
||||
if (action.endTime)
|
||||
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.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</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])} />}
|
||||
{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?.()}>
|
||||
{!!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>}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@
|
|||
let textarea = document.querySelector('textarea');
|
||||
textarea.focus();
|
||||
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 => {
|
||||
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 => {
|
||||
log('Keyup:', event.key, event.code, event.which, modifiers(event));
|
||||
log('Keyup:', event.key, event.code, getLocation(event), modifiers(event));
|
||||
});
|
||||
function modifiers(event) {
|
||||
let m = [];
|
||||
|
|
@ -28,6 +28,15 @@
|
|||
m.push('Shift')
|
||||
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) {
|
||||
console.log.apply(console, args);
|
||||
result += args.join(' ') + '\n';
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ class ExpectationReporter implements Reporter {
|
|||
const key = test.titlePath().slice(2).join(' › ');
|
||||
if (outcome === 'timeout')
|
||||
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());
|
||||
keys.sort();
|
||||
|
|
|
|||
|
|
@ -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-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/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 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 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/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]
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ it('should pass firefox user preferences', async ({ browserType, mode }) => {
|
|||
const browser = await browserType.launch({
|
||||
firefoxUserPrefs: {
|
||||
'network.proxy.type': 1,
|
||||
'network.proxy.http': '127.0.0.1',
|
||||
'network.proxy.http_port': 3333,
|
||||
'network.proxy.ssl': '127.0.0.1',
|
||||
'network.proxy.ssl_port': 3333,
|
||||
}
|
||||
});
|
||||
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');
|
||||
await browser.close();
|
||||
});
|
||||
|
|
@ -36,10 +36,10 @@ it('should pass firefox user preferences in persistent', async ({ mode, launchPe
|
|||
const { page } = await launchPersistent({
|
||||
firefoxUserPrefs: {
|
||||
'network.proxy.type': 1,
|
||||
'network.proxy.http': '127.0.0.1',
|
||||
'network.proxy.http_port': 3333,
|
||||
'network.proxy.ssl': '127.0.0.1',
|
||||
'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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -459,8 +459,13 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
const cli = runCLI([`--save-storage=${storageFileName}`, `--save-har=${harFileName}`]);
|
||||
await cli.waitFor(`import { test, expect } from '@playwright/test'`);
|
||||
await cli.process.kill('SIGINT');
|
||||
const { exitCode } = await cli.process.exited;
|
||||
expect(exitCode).toBe(130);
|
||||
const { exitCode, signal } = await cli.process.exited;
|
||||
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(harFileName)).toBeTruthy();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
await page.setContent(`
|
||||
<button popovertarget="pop">Click me</button>
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 474 B |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 35 KiB |
|
|
@ -14,6 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stripVTControlCharacters } from 'node:util';
|
||||
import { stripAnsi } from '../config/utils';
|
||||
import { test, expect } from './pageTest';
|
||||
|
||||
|
|
@ -240,10 +241,45 @@ test.describe('toHaveURL', () => {
|
|||
await expect(page).toHaveURL('data:text/html,<div>A</div>');
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
await page.goto('data:text/html,<div>B</div>');
|
||||
test('fail string', async ({ page }) => {
|
||||
await page.goto('data:text/html,<div>A</div>');
|
||||
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 }) => {
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 311 B |
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -18,10 +18,9 @@
|
|||
import { test as it, expect } from './pageTest';
|
||||
|
||||
it('should check the box @smoke', async ({ page }) => {
|
||||
await page.setContent(`<div class='middle selected row' id='component'></div>`);
|
||||
const locator = page.locator('#component');
|
||||
await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
|
||||
await expect(locator).toHaveClass('middle selected row');
|
||||
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
||||
await page.check('input');
|
||||
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not check the checked box', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -93,18 +93,18 @@ it('should report shiftKey', async ({ page, server, browserName, platform }) =>
|
|||
const codeForKey = { 'Shift': 16, 'Alt': 18, 'Control': 17 };
|
||||
for (const modifierKey in codeForKey) {
|
||||
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('!');
|
||||
// Shift+! will generate a keypress
|
||||
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
|
||||
expect(await page.evaluate('getResult()')).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']');
|
||||
expect(await page.evaluate('getResult()')).toBe('Keydown: ! Digit1 STANDARD [' + modifierKey + ']');
|
||||
|
||||
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);
|
||||
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');
|
||||
const keyboard = page.keyboard;
|
||||
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');
|
||||
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(';');
|
||||
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(';');
|
||||
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');
|
||||
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');
|
||||
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 }) => {
|
||||
await page.goto(server.PREFIX + '/input/keyboard.html');
|
||||
await page.keyboard.type('!');
|
||||
expect(await page.evaluate('getResult()')).toBe(
|
||||
['Keydown: ! Digit1 49 []',
|
||||
'Keypress: ! Digit1 33 33 []',
|
||||
'Keyup: ! Digit1 49 []'].join('\n'));
|
||||
['Keydown: ! Digit1 STANDARD []',
|
||||
'Keypress: ! Digit1 STANDARD 33 []',
|
||||
'Keyup: ! Digit1 STANDARD []'].join('\n'));
|
||||
await page.keyboard.type('^');
|
||||
expect(await page.evaluate('getResult()')).toBe(
|
||||
['Keydown: ^ Digit6 54 []',
|
||||
'Keypress: ^ Digit6 94 94 []',
|
||||
'Keyup: ^ Digit6 54 []'].join('\n'));
|
||||
['Keydown: ^ Digit6 STANDARD []',
|
||||
'Keypress: ^ Digit6 STANDARD 94 []',
|
||||
'Keyup: ^ Digit6 STANDARD []'].join('\n'));
|
||||
});
|
||||
|
||||
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 page.keyboard.type('~');
|
||||
expect(await page.evaluate('getResult()')).toBe(
|
||||
['Keydown: Shift ShiftLeft 16 [Shift]',
|
||||
'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode
|
||||
'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode
|
||||
'Keyup: ~ Backquote 192 [Shift]'].join('\n'));
|
||||
['Keydown: Shift ShiftLeft LEFT [Shift]',
|
||||
'Keydown: ~ Backquote STANDARD [Shift]',
|
||||
'Keypress: ~ Backquote STANDARD 126 [Shift]',
|
||||
'Keyup: ~ Backquote STANDARD [Shift]'].join('\n'));
|
||||
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.keyboard.press('+');
|
||||
expect(await page.evaluate('getResult()')).toBe(
|
||||
['Keydown: + Equal 187 []', // 192 is ` keyCode
|
||||
'Keypress: + Equal 43 43 []', // 126 is ~ charCode
|
||||
'Keyup: + Equal 187 []'].join('\n'));
|
||||
['Keydown: + Equal STANDARD []',
|
||||
'Keypress: + Equal STANDARD 43 []',
|
||||
'Keyup: + Equal STANDARD []'].join('\n'));
|
||||
});
|
||||
|
||||
it('should press shift plus', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/input/keyboard.html');
|
||||
await page.keyboard.press('Shift++');
|
||||
expect(await page.evaluate('getResult()')).toBe(
|
||||
['Keydown: Shift ShiftLeft 16 [Shift]',
|
||||
'Keydown: + Equal 187 [Shift]', // 192 is ` keyCode
|
||||
'Keypress: + Equal 43 43 [Shift]', // 126 is ~ charCode
|
||||
'Keyup: + Equal 187 [Shift]',
|
||||
'Keyup: Shift ShiftLeft 16 []'].join('\n'));
|
||||
['Keydown: Shift ShiftLeft LEFT [Shift]',
|
||||
'Keydown: + Equal STANDARD [Shift]',
|
||||
'Keypress: + Equal STANDARD 43 [Shift]',
|
||||
'Keyup: + Equal STANDARD [Shift]',
|
||||
'Keyup: Shift ShiftLeft LEFT []'].join('\n'));
|
||||
});
|
||||
|
||||
it('should support plus-separated modifiers', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/input/keyboard.html');
|
||||
await page.keyboard.press('Shift+~');
|
||||
expect(await page.evaluate('getResult()')).toBe(
|
||||
['Keydown: Shift ShiftLeft 16 [Shift]',
|
||||
'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode
|
||||
'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode
|
||||
'Keyup: ~ Backquote 192 [Shift]',
|
||||
'Keyup: Shift ShiftLeft 16 []'].join('\n'));
|
||||
['Keydown: Shift ShiftLeft LEFT [Shift]',
|
||||
'Keydown: ~ Backquote STANDARD [Shift]',
|
||||
'Keypress: ~ Backquote STANDARD 126 [Shift]',
|
||||
'Keyup: ~ Backquote STANDARD [Shift]',
|
||||
'Keyup: Shift ShiftLeft LEFT []'].join('\n'));
|
||||
});
|
||||
|
||||
it('should support multiple plus-separated modifiers', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/input/keyboard.html');
|
||||
await page.keyboard.press('Control+Shift+~');
|
||||
expect(await page.evaluate('getResult()')).toBe(
|
||||
['Keydown: Control ControlLeft 17 [Control]',
|
||||
'Keydown: Shift ShiftLeft 16 [Control Shift]',
|
||||
'Keydown: ~ Backquote 192 [Control Shift]', // 192 is ` keyCode
|
||||
'Keyup: ~ Backquote 192 [Control Shift]',
|
||||
'Keyup: Shift ShiftLeft 16 [Control]',
|
||||
'Keyup: Control ControlLeft 17 []'].join('\n'));
|
||||
['Keydown: Control ControlLeft LEFT [Control]',
|
||||
'Keydown: Shift ShiftLeft LEFT [Control Shift]',
|
||||
'Keydown: ~ Backquote STANDARD [Control Shift]',
|
||||
'Keyup: ~ Backquote STANDARD [Control Shift]',
|
||||
'Keyup: Shift ShiftLeft LEFT [Control]',
|
||||
'Keyup: Control ControlLeft LEFT []'].join('\n'));
|
||||
});
|
||||
|
||||
it('should shift raw codes', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/input/keyboard.html');
|
||||
await page.keyboard.press('Shift+Digit3');
|
||||
expect(await page.evaluate('getResult()')).toBe(
|
||||
['Keydown: Shift ShiftLeft 16 [Shift]',
|
||||
'Keydown: # Digit3 51 [Shift]', // 51 is # keyCode
|
||||
'Keypress: # Digit3 35 35 [Shift]', // 35 is # charCode
|
||||
'Keyup: # Digit3 51 [Shift]',
|
||||
'Keyup: Shift ShiftLeft 16 []'].join('\n'));
|
||||
['Keydown: Shift ShiftLeft LEFT [Shift]',
|
||||
'Keydown: # Digit3 STANDARD [Shift]',
|
||||
'Keypress: # Digit3 STANDARD 35 [Shift]',
|
||||
'Keyup: # Digit3 STANDARD [Shift]',
|
||||
'Keyup: Shift ShiftLeft LEFT []'].join('\n'));
|
||||
});
|
||||
|
||||
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.keyboard.press('Escape');
|
||||
expect(await page.evaluate('getResult()')).toBe(`
|
||||
Keydown: Escape Escape 27 []
|
||||
Keyup: Escape Escape 27 []
|
||||
Keydown: Escape Escape STANDARD []
|
||||
Keyup: Escape Escape STANDARD []
|
||||
`.trim());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
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`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -543,11 +543,31 @@ test('should respect expect.timeout', async ({ runInlineTest }) => {
|
|||
'playwright.config.js': `module.exports = { expect: { timeout: 1000 } }`,
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { stripVTControlCharacters } from 'node:util';
|
||||
|
||||
test('timeout', 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(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>');
|
||||
});
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -341,6 +341,36 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
|
|||
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) => {
|
||||
const EXPECTED_SNAPSHOT = 'Hello world';
|
||||
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.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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|||
// END: Reserved CI
|
||||
PW_TEST_HTML_REPORT_OPEN: undefined,
|
||||
PLAYWRIGHT_HTML_OPEN: undefined,
|
||||
PW_TEST_DEBUG_REPORTERS: undefined,
|
||||
PW_TEST_REPORTER: undefined,
|
||||
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
||||
PW_TEST_SOURCE_TRANSFORM: undefined,
|
||||
|
|
|
|||
|
|
@ -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(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]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.50.0-alpha-2025-01-17"
|
||||
"@playwright/test": "1.50.0-beta-1737557690000"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.50.0-alpha-2025-01-17",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-alpha-2025-01-17.tgz",
|
||||
"integrity": "sha512-fMUwMcP0YE2knged9GJXqv3fpT2xoywTtqYaSzpZmjnNESF+CUUAGY2hHm9/fz/v9ijcjyd62hYFbqS5KeKuHQ==",
|
||||
"version": "1.50.0-beta-1737557690000",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-beta-1737557690000.tgz",
|
||||
"integrity": "sha512-p6iaLrgsPatz9WqQMtxQyE9kElq8+Ae/N5i1+UF6+vxQdGpGSppk4+V4TttL8iMJhxvO/W3o/0vX8viDOVk1tg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.50.0-alpha-2025-01-17"
|
||||
"playwright": "1.50.0-beta-1737557690000"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -38,12 +38,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.50.0-alpha-2025-01-17",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-alpha-2025-01-17.tgz",
|
||||
"integrity": "sha512-LRavQ9Qu27nHvJ57f+7UDBTAEWhGKV+MS2qLAJpF8HXtfSMVlLK82W9Oba41lCNUzgLoAuFv0wCO/RcHqLz7yQ==",
|
||||
"version": "1.50.0-beta-1737557690000",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-beta-1737557690000.tgz",
|
||||
"integrity": "sha512-cgRCY5Gw0qZeqtSwvjMVVzUPQ19xLC6Z6i2oGa2Su2b4CO9qkceace1+Qe98RuCeU5nCHXbNTsPU6gue+yxagg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-alpha-2025-01-17"
|
||||
"playwright-core": "1.50.0-beta-1737557690000"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -56,9 +56,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.50.0-alpha-2025-01-17",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-alpha-2025-01-17.tgz",
|
||||
"integrity": "sha512-XkoLZ+7J5ybDq68xSlofPziH1Y8It9LpMisxtBfebjKWbVY8BzctlB1Da9udKDP0oWQPNq4tUnwW0hkeET3lUg==",
|
||||
"version": "1.50.0-beta-1737557690000",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-beta-1737557690000.tgz",
|
||||
"integrity": "sha512-pr4ZXZmn+hhFrWEIoPQZgxxwkFLPVhMH3uB5eL+SPUKLtFv0WhOoo1PHUUqinMumj84bhhYS6ODg2pPPGPG7sA==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
|
@ -70,11 +70,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": {
|
||||
"version": "1.50.0-alpha-2025-01-17",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-alpha-2025-01-17.tgz",
|
||||
"integrity": "sha512-fMUwMcP0YE2knged9GJXqv3fpT2xoywTtqYaSzpZmjnNESF+CUUAGY2hHm9/fz/v9ijcjyd62hYFbqS5KeKuHQ==",
|
||||
"version": "1.50.0-beta-1737557690000",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-beta-1737557690000.tgz",
|
||||
"integrity": "sha512-p6iaLrgsPatz9WqQMtxQyE9kElq8+Ae/N5i1+UF6+vxQdGpGSppk4+V4TttL8iMJhxvO/W3o/0vX8viDOVk1tg==",
|
||||
"requires": {
|
||||
"playwright": "1.50.0-alpha-2025-01-17"
|
||||
"playwright": "1.50.0-beta-1737557690000"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
|
|
@ -84,18 +84,18 @@
|
|||
"optional": true
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.50.0-alpha-2025-01-17",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-alpha-2025-01-17.tgz",
|
||||
"integrity": "sha512-LRavQ9Qu27nHvJ57f+7UDBTAEWhGKV+MS2qLAJpF8HXtfSMVlLK82W9Oba41lCNUzgLoAuFv0wCO/RcHqLz7yQ==",
|
||||
"version": "1.50.0-beta-1737557690000",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-beta-1737557690000.tgz",
|
||||
"integrity": "sha512-cgRCY5Gw0qZeqtSwvjMVVzUPQ19xLC6Z6i2oGa2Su2b4CO9qkceace1+Qe98RuCeU5nCHXbNTsPU6gue+yxagg==",
|
||||
"requires": {
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.50.0-alpha-2025-01-17"
|
||||
"playwright-core": "1.50.0-beta-1737557690000"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.50.0-alpha-2025-01-17",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-alpha-2025-01-17.tgz",
|
||||
"integrity": "sha512-XkoLZ+7J5ybDq68xSlofPziH1Y8It9LpMisxtBfebjKWbVY8BzctlB1Da9udKDP0oWQPNq4tUnwW0hkeET3lUg=="
|
||||
"version": "1.50.0-beta-1737557690000",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-beta-1737557690000.tgz",
|
||||
"integrity": "sha512-pr4ZXZmn+hhFrWEIoPQZgxxwkFLPVhMH3uB5eL+SPUKLtFv0WhOoo1PHUUqinMumj84bhhYS6ODg2pPPGPG7sA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.50.0-alpha-2025-01-17"
|
||||
"@playwright/test": "1.50.0-beta-1737557690000"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -398,7 +398,7 @@ test('should show custom fixture titles in actions tree', async ({ runUITest })
|
|||
const { page } = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
|
||||
|
||||
const test = base.extend({
|
||||
fixture1: [async ({}, use) => {
|
||||
await use();
|
||||
|
|
@ -457,7 +457,7 @@ test('attachments tab shows all but top-level .push attachments', async ({ runUI
|
|||
- tree:
|
||||
- treeitem /step/:
|
||||
- group:
|
||||
- treeitem /attach \\"foo-attach\\"/
|
||||
- treeitem /attach \\"foo-attach\\"/
|
||||
- treeitem /attach \\"bar-push\\"/
|
||||
- treeitem /attach \\"bar-attach\\"/
|
||||
`);
|
||||
|
|
@ -470,3 +470,32 @@ test('attachments tab shows all but top-level .push attachments', async ({ runUI
|
|||
- 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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -454,6 +454,50 @@ test('should generate baseline for input values', async ({ runInlineTest }, test
|
|||
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) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
|
|
@ -617,4 +661,45 @@ test.describe('update-source-method', () => {
|
|||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ set -x
|
|||
|
||||
trap "cd $(pwd -P)" EXIT
|
||||
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")"
|
||||
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
||||
|
|
|
|||
|
|
@ -5,24 +5,20 @@ The data is consumed by https://devops.playwright.dev/flakiness.html
|
|||
|
||||
## Publish
|
||||
|
||||
Azure Functions Core Tools is not available on macOS M1 yet, so we use GitHub Codespaces to publish the function.
|
||||
|
||||
### 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):
|
||||
- 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):
|
||||
```
|
||||
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
|
||||
mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
|
||||
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'
|
||||
apt-get update && apt-get install azure-functions-core-tools-4 sudo
|
||||
brew tap azure/functions
|
||||
brew install azure-functions-core-tools@4
|
||||
# if upgrading on a machine that has 2.x or 3.x installed:
|
||||
brew link --overwrite azure-functions-core-tools@4
|
||||
```
|
||||
- Install Azure CLI:
|
||||
```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
|
||||
az login --use-device-code
|
||||
az login
|
||||
```
|
||||
- Install NPM Deps (`node_modules/` folder will be published as-is):
|
||||
```
|
||||
|
|
|
|||