Merge branch 'microsoft:main' into test-config-populate-git-info
This commit is contained in:
commit
203951ec73
1
.github/workflows/tests_bidi.yml
vendored
1
.github/workflows/tests_bidi.yml
vendored
|
|
@ -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
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
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
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
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
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
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
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: |
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -35,4 +35,3 @@ test-results
|
|||
.cache/
|
||||
.eslintcache
|
||||
playwright.env
|
||||
firefox
|
||||
|
|
|
|||
35
FILING_ISSUES.md
Normal file
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,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;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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];
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -2274,13 +2274,8 @@ assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.Match
|
|||
* langs: js
|
||||
- `name` <[string]>
|
||||
|
||||
Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not specified.
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.path
|
||||
* since: v1.50
|
||||
- `path` <[string]>
|
||||
|
||||
Path to the YAML snapshot file.
|
||||
Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test.
|
||||
Generates sequential names if not specified.
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.50
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1822,7 +1822,7 @@ Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout
|
|||
* since: v1.50
|
||||
- `timeout` <[float]>
|
||||
|
||||
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
|
||||
The maximum time, in milliseconds, allowed for the step to complete. If the step does not complete within the specified timeout, the [`method: Test.step`] method will throw a [TimeoutError]. Defaults to `0` (no timeout).
|
||||
|
||||
## method: Test.use
|
||||
* since: v1.10
|
||||
|
|
|
|||
|
|
@ -645,7 +645,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
|
||||
|
||||
|
|
|
|||
78
package-lock.json
generated
78
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "playwright-internal",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
@ -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",
|
||||
|
|
@ -7751,10 +7753,10 @@
|
|||
"version": "0.0.0"
|
||||
},
|
||||
"packages/playwright": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7768,11 +7770,11 @@
|
|||
},
|
||||
"packages/playwright-browser-chromium": {
|
||||
"name": "@playwright/browser-chromium",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -7780,11 +7782,11 @@
|
|||
},
|
||||
"packages/playwright-browser-firefox": {
|
||||
"name": "@playwright/browser-firefox",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -7792,22 +7794,22 @@
|
|||
},
|
||||
"packages/playwright-browser-webkit": {
|
||||
"name": "@playwright/browser-webkit",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-chromium": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7817,7 +7819,7 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-core": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
|
@ -7828,12 +7830,12 @@
|
|||
},
|
||||
"packages/playwright-ct-core": {
|
||||
"name": "@playwright/experimental-ct-core",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.50.0-next",
|
||||
"playwright-core": "1.50.0-next",
|
||||
"vite": "^5.2.8"
|
||||
"playwright": "1.51.0-next",
|
||||
"playwright-core": "1.51.0-next",
|
||||
"vite": "^5.4.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -7841,10 +7843,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react": {
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.51.0-next",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7856,10 +7858,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react17": {
|
||||
"name": "@playwright/experimental-ct-react17",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.51.0-next",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7871,10 +7873,10 @@
|
|||
},
|
||||
"packages/playwright-ct-svelte": {
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.51.0-next",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7889,10 +7891,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue": {
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.51.0-next",
|
||||
"@vitejs/plugin-vue": "^5.2.0"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7903,11 +7905,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-firefox": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7918,10 +7920,10 @@
|
|||
},
|
||||
"packages/playwright-test": {
|
||||
"name": "@playwright/test",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.50.0-next"
|
||||
"playwright": "1.51.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7931,11 +7933,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-webkit": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"private": true,
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -60,11 +60,6 @@
|
|||
color: var(--color-scale-orange-6);
|
||||
border: 1px solid var(--color-scale-orange-4);
|
||||
}
|
||||
.label-color-gray {
|
||||
background-color: var(--color-scale-gray-0);
|
||||
color: var(--color-scale-gray-6);
|
||||
border: 1px solid var(--color-scale-gray-4);
|
||||
}
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: dark) {
|
||||
|
|
@ -98,11 +93,6 @@
|
|||
color: var(--color-scale-orange-2);
|
||||
border: 1px solid var(--color-scale-orange-4);
|
||||
}
|
||||
.label-color-gray {
|
||||
background-color: var(--color-scale-gray-9);
|
||||
color: var(--color-scale-gray-2);
|
||||
border: 1px solid var(--color-scale-gray-4);
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-body {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { TreeItem } from './treeItem';
|
|||
import { CopyToClipboard } from './copyToClipboard';
|
||||
import './links.css';
|
||||
import { linkifyText } from '@web/renderUtils';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
import { clsx, useFlash } from '@web/uiUtils';
|
||||
|
||||
export function navigate(href: string | URL) {
|
||||
window.history.pushState({}, '', href);
|
||||
|
|
@ -73,7 +73,8 @@ export const AttachmentLink: React.FunctionComponent<{
|
|||
linkName?: string,
|
||||
openInNewTab?: boolean,
|
||||
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
|
||||
const isAnchored = useIsAnchored('attachment-' + result.attachments.indexOf(attachment));
|
||||
const [flash, triggerFlash] = useFlash();
|
||||
useAnchor('attachment-' + result.attachments.indexOf(attachment), triggerFlash);
|
||||
return <TreeItem title={<span>
|
||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
||||
|
|
@ -84,7 +85,7 @@ export const AttachmentLink: React.FunctionComponent<{
|
|||
)}
|
||||
</span>} loadChildren={attachment.body ? () => {
|
||||
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
||||
} : undefined} depth={0} style={{ lineHeight: '32px' }} selected={isAnchored}></TreeItem>;
|
||||
} : undefined} depth={0} style={{ lineHeight: '32px' }} flash={flash}></TreeItem>;
|
||||
};
|
||||
|
||||
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
||||
|
|
@ -118,12 +119,12 @@ const kMissingContentType = 'x-playwright/missing';
|
|||
|
||||
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
||||
|
||||
export function useAnchor(id: AnchorID, onReveal: () => void) {
|
||||
export function useAnchor(id: AnchorID, onReveal: React.EffectCallback) {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const isAnchored = useIsAnchored(id);
|
||||
React.useEffect(() => {
|
||||
if (isAnchored)
|
||||
onReveal();
|
||||
return onReveal();
|
||||
}, [isAnchored, onReveal, searchParams]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ const StepTreeItem: React.FC<{
|
|||
}> = ({ test, step, result, depth }) => {
|
||||
return <TreeItem title={<span aria-label={step.title}>
|
||||
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
||||
{step.attachments.length > 0 && <a style={{ float: 'right' }} title={`reveal attachment`} href={testResultHref({ test, result, anchor: `attachment-${step.attachments[0]}` })} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
|
||||
{statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))}
|
||||
<span>{step.title}</span>
|
||||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||
|
|
@ -183,20 +184,6 @@ const StepTreeItem: React.FC<{
|
|||
</span>} loadChildren={step.steps.length || step.snippet ? () => {
|
||||
const snippet = step.snippet ? [<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>] : [];
|
||||
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
||||
const attachments = step.attachments.map(attachmentIndex => (
|
||||
<a key={'' + attachmentIndex}
|
||||
href={testResultHref({ test, result, anchor: `attachment-${attachmentIndex}` })}
|
||||
style={{ paddingLeft: depth * 22 + 4, textDecoration: 'none' }}
|
||||
>
|
||||
<span
|
||||
style={{ margin: '8px 0 0 8px', padding: '2px 10px', cursor: 'pointer' }}
|
||||
className='label label-color-gray'
|
||||
title={`see "${result.attachments[attachmentIndex].name}"`}
|
||||
>
|
||||
{icons.attachment()}{result.attachments[attachmentIndex].name}
|
||||
</span>
|
||||
</a>
|
||||
));
|
||||
return snippet.concat(steps, attachments);
|
||||
return snippet.concat(steps);
|
||||
} : undefined} depth={depth}/>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,11 +25,14 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-item-title.selected {
|
||||
text-decoration: underline var(--color-underlinenav-icon);
|
||||
text-decoration-thickness: 1.5px;
|
||||
}
|
||||
|
||||
.tree-item-body {
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
.yellow-flash {
|
||||
animation: yellowflash-bg 2s;
|
||||
}
|
||||
@keyframes yellowflash-bg {
|
||||
from { background: var(--color-attention-subtle); }
|
||||
to { background: transparent; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ export const TreeItem: React.FunctionComponent<{
|
|||
onClick?: () => void,
|
||||
expandByDefault?: boolean,
|
||||
depth: number,
|
||||
selected?: boolean,
|
||||
style?: React.CSSProperties,
|
||||
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => {
|
||||
flash?: boolean
|
||||
}> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => {
|
||||
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
||||
return <div className={'tree-item'} style={style}>
|
||||
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||
return <div className={clsx('tree-item', flash && 'yellow-flash')} style={style}>
|
||||
<span className='tree-item-title' style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||
{loadChildren && !!expanded && icons.downArrow()}
|
||||
{loadChildren && !expanded && icons.rightArrow()}
|
||||
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-chromium",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "Playwright package that automatically installs Chromium",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-firefox",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "Playwright package that automatically installs Firefox",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-webkit",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "Playwright package that automatically installs WebKit",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "A high-level API to automate Chromium",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1293",
|
||||
"revision": "1297",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "133.0.6943.0"
|
||||
"browserVersion": "134.0.6974.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2123",
|
||||
"revision": "2125",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"debian11-x64": "2105",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ commandWithOpenOptions('codegen [url]', 'open page and generate code for user ac
|
|||
[
|
||||
['-o, --output <file name>', 'saves the generated script to a file'],
|
||||
['--target <language>', `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()],
|
||||
['--save-trace <filename>', 'record a trace for the session and save it to a file'],
|
||||
['--test-id-attribute <attributeName>', 'use the specified attribute to generate data test ID selectors'],
|
||||
]).action(function(url, options) {
|
||||
codegen(options, url).catch(logErrorAndExit);
|
||||
|
|
@ -353,7 +352,6 @@ type Options = {
|
|||
saveHar?: string;
|
||||
saveHarGlob?: string;
|
||||
saveStorage?: string;
|
||||
saveTrace?: string;
|
||||
timeout: string;
|
||||
timezone?: string;
|
||||
viewportSize?: string;
|
||||
|
|
@ -508,8 +506,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
if (closingBrowser)
|
||||
return;
|
||||
closingBrowser = true;
|
||||
if (options.saveTrace)
|
||||
await context.tracing.stop({ path: options.saveTrace });
|
||||
if (options.saveStorage)
|
||||
await context.storageState({ path: options.saveStorage }).catch(e => null);
|
||||
if (options.saveHar)
|
||||
|
|
@ -536,9 +532,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
context.setDefaultTimeout(timeout);
|
||||
context.setDefaultNavigationTimeout(timeout);
|
||||
|
||||
if (options.saveTrace)
|
||||
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
|
||||
// Omit options that we add automatically for presentation purpose.
|
||||
delete launchOptions.headless;
|
||||
delete launchOptions.executablePath;
|
||||
|
|
|
|||
|
|
@ -133,6 +133,12 @@ function defaultProfilePreferences(
|
|||
'dom.max_chrome_script_run_time': 0,
|
||||
'dom.max_script_run_time': 0,
|
||||
|
||||
// Disable background timer throttling to allow tests to run in parallel
|
||||
// without a decrease in performance.
|
||||
'dom.min_background_timeout_value': 0,
|
||||
'dom.min_background_timeout_value_without_budget_throttling': 0,
|
||||
'dom.timeout.enable_budget_timer_throttling': false,
|
||||
|
||||
// Only load extensions from the application and user profile
|
||||
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
|
||||
'extensions.autoDisableScopes': 0,
|
||||
|
|
@ -175,6 +181,9 @@ function defaultProfilePreferences(
|
|||
// Show chrome errors and warnings in the error console
|
||||
'javascript.options.showInConsole': true,
|
||||
|
||||
// Do not throttle rendering (requestAnimationFrame) in background tabs
|
||||
'layout.testing.top-level-always-active': true,
|
||||
|
||||
// Disable download and usage of OpenH264: and Widevine plugins
|
||||
'media.gmp-manager.updateEnabled': false,
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -27,9 +27,8 @@ import { Debugger } from './debugger';
|
|||
import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
|
||||
import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder';
|
||||
import type { IRecorderAppFactory, IRecorderApp, IRecorder } from './recorder/recorderFrontend';
|
||||
import { metadataToCallLog } from './recorder/recorderUtils';
|
||||
import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils';
|
||||
import type * as actions from '@recorder/actions';
|
||||
import { buildFullSelector } from '../utils/isomorphic/recorderUtils';
|
||||
import { stringifySelector } from '../utils/isomorphic/selectorParser';
|
||||
import type { Frame } from './frames';
|
||||
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
|
||||
|
|
|
|||
|
|
@ -20,10 +20,8 @@ import type { Page } from '../page';
|
|||
import type { Signal } from '../../../../recorder/src/actions';
|
||||
import type * as actions from '@recorder/actions';
|
||||
import { monotonicTime } from '../../utils/time';
|
||||
import { callMetadataForAction, collapseActions } from './recorderUtils';
|
||||
import { serializeError } from '../errors';
|
||||
import { collapseActions } from './recorderUtils';
|
||||
import { performAction } from './recorderRunner';
|
||||
import type { CallMetadata } from '@protocol/callMetadata';
|
||||
import { isUnderTest } from '../../utils/debug';
|
||||
|
||||
export class RecorderCollection extends EventEmitter {
|
||||
|
|
@ -46,8 +44,8 @@ export class RecorderCollection extends EventEmitter {
|
|||
}
|
||||
|
||||
async performAction(actionInContext: actions.ActionInContext) {
|
||||
await this._addAction(actionInContext, async callMetadata => {
|
||||
await performAction(callMetadata, this._pageAliases, actionInContext);
|
||||
await this._addAction(actionInContext, async () => {
|
||||
await performAction(this._pageAliases, actionInContext);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +58,7 @@ export class RecorderCollection extends EventEmitter {
|
|||
this._addAction(actionInContext).catch(() => {});
|
||||
}
|
||||
|
||||
private async _addAction(actionInContext: actions.ActionInContext, callback?: (callMetadata: CallMetadata) => Promise<void>) {
|
||||
private async _addAction(actionInContext: actions.ActionInContext, callback?: () => Promise<void>) {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
if (actionInContext.action.name === 'openPage' || actionInContext.action.name === 'closePage') {
|
||||
|
|
@ -69,18 +67,10 @@ export class RecorderCollection extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext);
|
||||
await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata);
|
||||
this._actions.push(actionInContext);
|
||||
this._fireChange();
|
||||
const error = await callback?.(callMetadata).catch((e: Error) => e);
|
||||
callMetadata.endTime = monotonicTime();
|
||||
actionInContext.endTime = callMetadata.endTime;
|
||||
callMetadata.error = error ? serializeError(error) : undefined;
|
||||
// Do not wait for onAfterCall so that performAction returned immediately after the action.
|
||||
mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata).then(() => {
|
||||
this._fireChange();
|
||||
}).catch(() => {});
|
||||
await callback?.().catch();
|
||||
actionInContext.endTime = monotonicTime();
|
||||
}
|
||||
|
||||
signal(pageAlias: string, frame: Frame, signal: Signal) {
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@
|
|||
|
||||
import { serializeExpectedTextValues } from '../../utils';
|
||||
import { toKeyboardModifiers } from '../codegen/language';
|
||||
import type { CallMetadata } from '../instrumentation';
|
||||
import { serverSideCallMetadata } from '../instrumentation';
|
||||
import type { Page } from '../page';
|
||||
import type * as actions from '@recorder/actions';
|
||||
import type * as types from '../types';
|
||||
import { mainFrameForAction } from './recorderUtils';
|
||||
import { buildFullSelector } from '../../utils/isomorphic/recorderUtils';
|
||||
import { buildFullSelector, mainFrameForAction } from './recorderUtils';
|
||||
|
||||
export async function performAction(callMetadata: CallMetadata, pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext) {
|
||||
export async function performAction(pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext) {
|
||||
const callMetadata = serverSideCallMetadata();
|
||||
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
|
||||
const { action } = actionInContext;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ import type { CallLog, CallLogStatus } from '@recorder/recorderTypes';
|
|||
import type { Page } from '../page';
|
||||
import type { Frame } from '../frames';
|
||||
import type * as actions from '@recorder/actions';
|
||||
import { createGuid } from '../../utils';
|
||||
import { buildFullSelector, traceParamsForAction } from '../../utils/isomorphic/recorderUtils';
|
||||
|
||||
export function buildFullSelector(framePath: string[], selector: string) {
|
||||
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
|
||||
}
|
||||
|
||||
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
|
||||
let title = metadata.apiName || metadata.method;
|
||||
|
|
@ -70,26 +72,6 @@ export async function frameForAction(pageAliases: Map<Page, string>, actionInCon
|
|||
return result.frame;
|
||||
}
|
||||
|
||||
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
|
||||
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
|
||||
const { method, apiName, params } = traceParamsForAction(actionInContext);
|
||||
|
||||
const callMetadata: CallMetadata = {
|
||||
id: `call@${createGuid()}`,
|
||||
apiName,
|
||||
objectId: mainFrame.guid,
|
||||
pageId: mainFrame._page.guid,
|
||||
frameId: mainFrame.guid,
|
||||
startTime: actionInContext.startTime,
|
||||
endTime: 0,
|
||||
type: 'Frame',
|
||||
method,
|
||||
params,
|
||||
log: [],
|
||||
};
|
||||
return { callMetadata, mainFrame };
|
||||
}
|
||||
|
||||
export function collapseActions(actions: actions.ActionInContext[]): actions.ActionInContext[] {
|
||||
const result: actions.ActionInContext[] = [];
|
||||
for (const action of actions) {
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
/**
|
||||
* 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 * as recorderActions from '@recorder/actions';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type * as types from '../../server/types';
|
||||
|
||||
export function buildFullSelector(framePath: string[], selector: string) {
|
||||
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
|
||||
}
|
||||
|
||||
const kDefaultTimeout = 5_000;
|
||||
|
||||
export function traceParamsForAction(actionInContext: recorderActions.ActionInContext): { method: string, apiName: string, params: any } {
|
||||
const { action } = actionInContext;
|
||||
|
||||
switch (action.name) {
|
||||
case 'navigate': {
|
||||
const params: channels.FrameGotoParams = {
|
||||
url: action.url,
|
||||
};
|
||||
return { method: 'goto', apiName: 'page.goto', params };
|
||||
}
|
||||
case 'openPage':
|
||||
case 'closePage':
|
||||
throw new Error('Not reached');
|
||||
}
|
||||
|
||||
const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
|
||||
switch (action.name) {
|
||||
case 'click': {
|
||||
const params: channels.FrameClickParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
modifiers: toKeyboardModifiers(action.modifiers),
|
||||
button: action.button,
|
||||
clickCount: action.clickCount,
|
||||
position: action.position,
|
||||
};
|
||||
return { method: 'click', apiName: 'locator.click', params };
|
||||
}
|
||||
case 'press': {
|
||||
const params: channels.FramePressParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'),
|
||||
};
|
||||
return { method: 'press', apiName: 'locator.press', params };
|
||||
}
|
||||
case 'fill': {
|
||||
const params: channels.FrameFillParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
value: action.text,
|
||||
};
|
||||
return { method: 'fill', apiName: 'locator.fill', params };
|
||||
}
|
||||
case 'setInputFiles': {
|
||||
const params: channels.FrameSetInputFilesParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
localPaths: action.files,
|
||||
};
|
||||
return { method: 'setInputFiles', apiName: 'locator.setInputFiles', params };
|
||||
}
|
||||
case 'check': {
|
||||
const params: channels.FrameCheckParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
};
|
||||
return { method: 'check', apiName: 'locator.check', params };
|
||||
}
|
||||
case 'uncheck': {
|
||||
const params: channels.FrameUncheckParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
};
|
||||
return { method: 'uncheck', apiName: 'locator.uncheck', params };
|
||||
}
|
||||
case 'select': {
|
||||
const params: channels.FrameSelectOptionParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
options: action.options.map(option => ({ value: option })),
|
||||
};
|
||||
return { method: 'selectOption', apiName: 'locator.selectOption', params };
|
||||
}
|
||||
case 'assertChecked': {
|
||||
const params: channels.FrameExpectParams = {
|
||||
selector: action.selector,
|
||||
expression: 'to.be.checked',
|
||||
isNot: !action.checked,
|
||||
timeout: kDefaultTimeout,
|
||||
};
|
||||
return { method: 'expect', apiName: 'expect.toBeChecked', params };
|
||||
}
|
||||
case 'assertText': {
|
||||
const params: channels.FrameExpectParams = {
|
||||
selector,
|
||||
expression: 'to.have.text',
|
||||
expectedText: [],
|
||||
isNot: false,
|
||||
timeout: kDefaultTimeout,
|
||||
};
|
||||
return { method: 'expect', apiName: 'expect.toContainText', params };
|
||||
}
|
||||
case 'assertValue': {
|
||||
const params: channels.FrameExpectParams = {
|
||||
selector,
|
||||
expression: 'to.have.value',
|
||||
expectedValue: undefined,
|
||||
isNot: false,
|
||||
timeout: kDefaultTimeout,
|
||||
};
|
||||
return { method: 'expect', apiName: 'expect.toHaveValue', params };
|
||||
}
|
||||
case 'assertVisible': {
|
||||
const params: channels.FrameExpectParams = {
|
||||
selector,
|
||||
expression: 'to.be.visible',
|
||||
isNot: false,
|
||||
timeout: kDefaultTimeout,
|
||||
};
|
||||
return { method: 'expect', apiName: 'expect.toBeVisible', params };
|
||||
}
|
||||
case 'assertSnapshot': {
|
||||
const params: channels.FrameExpectParams = {
|
||||
selector,
|
||||
expression: 'to.match.snapshot',
|
||||
expectedText: [],
|
||||
isNot: false,
|
||||
timeout: kDefaultTimeout,
|
||||
};
|
||||
return { method: 'expect', apiName: 'expect.toMatchAriaSnapshot', params };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] {
|
||||
const result: types.SmartKeyboardModifier[] = [];
|
||||
if (modifiers & 1)
|
||||
result.push('Alt');
|
||||
if (modifiers & 2)
|
||||
result.push('ControlOrMeta');
|
||||
if (modifiers & 4)
|
||||
result.push('ControlOrMeta');
|
||||
if (modifiers & 8)
|
||||
result.push('Shift');
|
||||
return result;
|
||||
}
|
||||
12
packages/playwright-core/types/types.d.ts
vendored
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).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-core",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "Playwright Component Testing Helpers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -26,8 +26,8 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next",
|
||||
"vite": "^5.2.8",
|
||||
"playwright": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next",
|
||||
"vite": "^5.4.14",
|
||||
"playwright": "1.51.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "Playwright Component Testing for React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.51.0-next",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-react17",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "Playwright Component Testing for React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.51.0-next",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "Playwright Component Testing for Svelte",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.51.0-next",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "Playwright Component Testing for Vue",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.51.0-next",
|
||||
"@vitejs/plugin-vue": "^5.2.0"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-firefox",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "A high-level API to automate Firefox",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/test",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
},
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"playwright": "1.50.0-next"
|
||||
"playwright": "1.51.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-webkit",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "A high-level API to automate WebKit",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.51.0-next",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.51.0-next"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
|
|
|
|||
|
|
@ -270,9 +270,20 @@ export class TestTypeImpl {
|
|||
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
|
||||
return await zones.run('stepZone', step, async () => {
|
||||
try {
|
||||
const result = await raceAgainstDeadline(async () => body(), options.timeout ? monotonicTime() + options.timeout : 0);
|
||||
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
|
||||
result = await raceAgainstDeadline(async () => {
|
||||
try {
|
||||
return await body();
|
||||
} catch (e) {
|
||||
// If the step timed out, the test fixtures will tear down, which in turn
|
||||
// will abort unfinished actions in the step body. Record such errors here.
|
||||
if (result?.timedOut)
|
||||
testInfo._failWithError(e);
|
||||
throw e;
|
||||
}
|
||||
}, options.timeout ? monotonicTime() + options.timeout : 0);
|
||||
if (result.timedOut)
|
||||
throw new errors.TimeoutError(`Step timeout ${options.timeout}ms exceeded.`);
|
||||
throw new errors.TimeoutError(`Step timeout of ${options.timeout}ms exceeded.`);
|
||||
step.complete({});
|
||||
return result.result;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -59,9 +59,7 @@ export async function toMatchAriaSnapshot(
|
|||
if (isString(expectedParam)) {
|
||||
expected = expectedParam;
|
||||
} else {
|
||||
if (expectedParam?.path) {
|
||||
expectedPath = expectedParam.path;
|
||||
} else if (expectedParam?.name) {
|
||||
if (expectedParam?.name) {
|
||||
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
|
||||
} else {
|
||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||
|
|
@ -136,7 +134,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,7 +282,7 @@ 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
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
16
packages/playwright/types/test.d.ts
vendored
16
packages/playwright/types/test.d.ts
vendored
|
|
@ -8715,16 +8715,11 @@ interface LocatorAssertions {
|
|||
*/
|
||||
toMatchAriaSnapshot(options?: {
|
||||
/**
|
||||
* Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not
|
||||
* specified.
|
||||
* Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test. Generates sequential
|
||||
* names if not specified.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Path to the YAML snapshot file.
|
||||
*/
|
||||
path?: string;
|
||||
|
||||
/**
|
||||
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
||||
*/
|
||||
|
|
@ -9668,9 +9663,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>}
|
||||
|
|
|
|||
|
|
@ -55,3 +55,11 @@
|
|||
a.codicon-cloud-download:hover{
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground)
|
||||
}
|
||||
|
||||
.yellow-flash {
|
||||
animation: yellowflash-bg 2s;
|
||||
}
|
||||
@keyframes yellowflash-bg {
|
||||
from { background: var(--vscode-peekViewEditor-matchHighlightBackground); }
|
||||
to { background: transparent; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,36 +17,38 @@
|
|||
import * as React from 'react';
|
||||
import './attachmentsTab.css';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
||||
import type { MultiTraceModel } from './modelUtil';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
||||
import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper';
|
||||
import { isTextualMimeType } from '@isomorphic/mimeType';
|
||||
import { Expandable } from '@web/components/expandable';
|
||||
import { linkifyText } from '@web/renderUtils';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
import { clsx, useFlash } from '@web/uiUtils';
|
||||
|
||||
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
||||
|
||||
type ExpandableAttachmentProps = {
|
||||
attachment: Attachment;
|
||||
reveal: boolean;
|
||||
highlight: boolean;
|
||||
reveal?: any;
|
||||
};
|
||||
|
||||
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal, highlight }) => {
|
||||
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal }) => {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
|
||||
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
||||
const [flash, triggerFlash] = useFlash();
|
||||
const ref = React.useRef<HTMLSpanElement>(null);
|
||||
|
||||
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
||||
const hasContent = !!attachment.sha1 || !!attachment.path;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (reveal)
|
||||
if (reveal) {
|
||||
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [reveal]);
|
||||
return triggerFlash();
|
||||
}
|
||||
}, [reveal, triggerFlash]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (expanded && attachmentText === null && placeholder === null) {
|
||||
|
|
@ -66,14 +68,14 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
|||
}, [attachmentText]);
|
||||
|
||||
const title = <span style={{ marginLeft: 5 }} ref={ref} aria-label={attachment.name}>
|
||||
<span className={clsx(highlight && 'attachment-title-highlight')}>{linkifyText(attachment.name)}</span>
|
||||
<span>{linkifyText(attachment.name)}</span>
|
||||
{hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
|
||||
</span>;
|
||||
|
||||
if (!isTextAttachment || !hasContent)
|
||||
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
||||
|
||||
return <>
|
||||
return <div className={clsx(flash && 'yellow-flash')}>
|
||||
<Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}>
|
||||
{placeholder && <i>{placeholder}</i>}
|
||||
</Expandable>
|
||||
|
|
@ -87,14 +89,13 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
|||
wrapLines={false}>
|
||||
</CodeMirrorWrapper>
|
||||
</div>}
|
||||
</>;
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const AttachmentsTab: React.FunctionComponent<{
|
||||
model: MultiTraceModel | undefined,
|
||||
selectedAction: ActionTraceEventInContext | undefined,
|
||||
revealedAttachment?: AfterActionTraceEventAttachment,
|
||||
}> = ({ model, selectedAction, revealedAttachment }) => {
|
||||
revealedAttachment?: [AfterActionTraceEventAttachment, number],
|
||||
}> = ({ model, revealedAttachment }) => {
|
||||
const { diffMap, screenshots, attachments } = React.useMemo(() => {
|
||||
const attachments = new Set<Attachment>();
|
||||
const screenshots = new Set<Attachment>();
|
||||
|
|
@ -153,8 +154,7 @@ export const AttachmentsTab: React.FunctionComponent<{
|
|||
return <div className='attachment-item' key={attachmentKey(a, i)}>
|
||||
<ExpandableAttachment
|
||||
attachment={a}
|
||||
highlight={selectedAction?.attachments?.some(selected => isEqualAttachment(a, selected)) ?? false}
|
||||
reveal={!!revealedAttachment && isEqualAttachment(a, revealedAttachment)}
|
||||
reveal={(!!revealedAttachment && isEqualAttachment(a, revealedAttachment[0])) ? revealedAttachment : undefined}
|
||||
/>
|
||||
</div>;
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
|
||||
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
||||
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
||||
const [revealedAttachment, setRevealedAttachment] = React.useState<AfterActionTraceEventAttachment | undefined>(undefined);
|
||||
const [revealedAttachment, setRevealedAttachment] = React.useState<[attachment: AfterActionTraceEventAttachment, renderCounter: number] | undefined>(undefined);
|
||||
const [highlightedCallId, setHighlightedCallId] = React.useState<string | undefined>();
|
||||
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
|
||||
|
|
@ -148,7 +148,12 @@ export const Workbench: React.FunctionComponent<{
|
|||
|
||||
const revealAttachment = React.useCallback((attachment: AfterActionTraceEventAttachment) => {
|
||||
selectPropertiesTab('attachments');
|
||||
setRevealedAttachment(attachment);
|
||||
setRevealedAttachment(currentValue => {
|
||||
if (!currentValue)
|
||||
return [attachment, 0];
|
||||
const revealCounter = currentValue[1];
|
||||
return [attachment, revealCounter + 1];
|
||||
});
|
||||
}, [selectPropertiesTab]);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -238,7 +243,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
id: 'attachments',
|
||||
title: 'Attachments',
|
||||
count: attachments.length,
|
||||
render: () => <AttachmentsTab model={model} selectedAction={selectedAction} revealedAttachment={revealedAttachment} />
|
||||
render: () => <AttachmentsTab model={model} revealedAttachment={revealedAttachment} />
|
||||
};
|
||||
|
||||
const tabs: TabbedPaneTabModel[] = [
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { EffectCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
// Recalculates the value when dependencies change.
|
||||
|
|
@ -224,3 +225,26 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) {
|
|||
|
||||
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
|
||||
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
|
||||
|
||||
/**
|
||||
* Manages flash animation state.
|
||||
* Calling `trigger` will turn `flash` to true for a second, and then back to false.
|
||||
* If `trigger` is called while a flash is ongoing, the ongoing flash will be cancelled and after 50ms a new flash is started.
|
||||
* @returns [flash, trigger]
|
||||
*/
|
||||
export function useFlash(): [boolean, EffectCallback] {
|
||||
const [flash, setFlash] = React.useState(false);
|
||||
const trigger = React.useCallback<React.EffectCallback>(() => {
|
||||
const timeouts: any[] = [];
|
||||
setFlash(currentlyFlashing => {
|
||||
timeouts.push(setTimeout(() => setFlash(false), 1000));
|
||||
if (!currentlyFlashing)
|
||||
return true;
|
||||
|
||||
timeouts.push(setTimeout(() => setFlash(true), 50));
|
||||
return false;
|
||||
});
|
||||
return () => timeouts.forEach(clearTimeout);
|
||||
}, [setFlash]);
|
||||
return [flash, trigger];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@ library/inspector/cli-codegen-3.spec.ts › cli codegen › should generate fram
|
|||
library/page-clock.spec.ts › popup › should run time before popup [timeout]
|
||||
library/page-clock.spec.ts › popup › should tick after popup [timeout]
|
||||
library/page-clock.spec.ts › popup › should tick before popup [timeout]
|
||||
library/popup.spec.ts › should not throttle rAF in the opener page [timeout]
|
||||
library/popup.spec.ts › should not throw when click closes popup [timeout]
|
||||
library/popup.spec.ts › should use viewport size from window features [timeout]
|
||||
library/trace-viewer.spec.ts › should serve css without content-type [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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -313,3 +313,46 @@ it('headless and headful should use same default fonts', async ({ page, browserN
|
|||
}
|
||||
await headlessBrowser.close();
|
||||
});
|
||||
|
||||
it('should have the same hyphen rendering on headless and headed', {
|
||||
annotation: {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/microsoft/playwright/issues/33590'
|
||||
}
|
||||
}, async ({ browserType, page, headless, server }) => {
|
||||
const content = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<style>
|
||||
.hyphenated {
|
||||
width: 100px;
|
||||
hyphens: auto;
|
||||
text-align: justify;
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hyphenated">
|
||||
supercalifragilisticexpialidocious
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
server.setRoute('/hyphenated.html', (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(content);
|
||||
});
|
||||
const oppositeBrowser = await browserType.launch({ headless: !headless });
|
||||
const oppositePage = await oppositeBrowser.newPage();
|
||||
await oppositePage.goto(server.PREFIX + '/hyphenated.html');
|
||||
await page.goto(server.PREFIX + '/hyphenated.html');
|
||||
|
||||
const [divHeight1, divHeight2] = await Promise.all([
|
||||
page.evaluate(() => document.querySelector('.hyphenated').getBoundingClientRect().height),
|
||||
oppositePage.evaluate(() => document.querySelector('.hyphenated').getBoundingClientRect().height),
|
||||
]);
|
||||
expect(divHeight1).toBe(divHeight2);
|
||||
await oppositeBrowser.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -451,27 +451,16 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
|
||||
});
|
||||
|
||||
test('should --save-trace', async ({ runCLI }, testInfo) => {
|
||||
const traceFileName = testInfo.outputPath('trace.zip');
|
||||
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
||||
autoExitWhen: ' ',
|
||||
});
|
||||
await cli.waitForCleanExit();
|
||||
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
|
||||
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
||||
|
||||
const traceFileName = testInfo.outputPath('trace.zip');
|
||||
const storageFileName = testInfo.outputPath('auth.json');
|
||||
const harFileName = testInfo.outputPath('har.har');
|
||||
const cli = runCLI([`--save-trace=${traceFileName}`, `--save-storage=${storageFileName}`, `--save-har=${harFileName}`]);
|
||||
const cli = runCLI([`--save-storage=${storageFileName}`, `--save-har=${harFileName}`]);
|
||||
await cli.waitFor(`import { test, expect } from '@playwright/test'`);
|
||||
await cli.process.kill('SIGINT');
|
||||
const { exitCode } = await cli.process.exited;
|
||||
expect(exitCode).toBe(130);
|
||||
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
||||
expect(fs.existsSync(storageFileName)).toBeTruthy();
|
||||
expect(fs.existsSync(harFileName)).toBeTruthy();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
- tree:
|
||||
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem "[icon-circle-outline] passes" [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- treeitem "[icon-circle-outline] fails"
|
||||
- treeitem "[icon-circle-outline] suite"
|
||||
- treeitem "[icon-circle-outline] b.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem "[icon-circle-outline] passes"
|
||||
- treeitem "[icon-circle-outline] fails"
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
- tree:
|
||||
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem "[icon-circle-outline] passes"
|
||||
- treeitem "[icon-circle-outline] fails" [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- treeitem "[icon-circle-outline] suite"
|
||||
- treeitem "[icon-circle-outline] b.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem "[icon-circle-outline] passes"
|
||||
- treeitem "[icon-circle-outline] fails"
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
- tree:
|
||||
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem "[icon-circle-outline] passes" [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- treeitem "[icon-circle-outline] fails"
|
||||
- treeitem "[icon-circle-outline] suite"
|
||||
- treeitem "[icon-circle-outline] b.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem "[icon-circle-outline] passes"
|
||||
- treeitem "[icon-circle-outline] fails"
|
||||
|
|
@ -42,24 +42,6 @@ test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
|
|||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should match snapshot with path', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'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({ path: path.resolve(__dirname, 'test.yml') });
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should generate multiple missing', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -959,10 +959,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
await showReport();
|
||||
await page.getByRole('link', { name: 'passing' }).click();
|
||||
|
||||
const attachment = page.getByTestId('attachments').getByText('foo-2', { exact: true });
|
||||
const attachment = page.getByText('foo-2', { exact: true });
|
||||
await expect(attachment).not.toBeInViewport();
|
||||
await page.getByLabel('attach "foo-2"').click();
|
||||
await page.getByTitle('see "foo-2"').click();
|
||||
await page.getByLabel(`attach "foo-2"`).getByTitle('reveal attachment').click();
|
||||
await expect(attachment).toBeInViewport();
|
||||
|
||||
await page.reload();
|
||||
|
|
@ -989,10 +988,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
await showReport();
|
||||
await page.getByRole('link', { name: 'passing' }).click();
|
||||
|
||||
const attachment = page.getByTestId('attachments').getByText('attachment', { exact: true });
|
||||
const attachment = page.getByText('attachment', { exact: true });
|
||||
await expect(attachment).not.toBeInViewport();
|
||||
await page.getByLabel('step').click();
|
||||
await page.getByTitle('see "attachment"').click();
|
||||
await page.getByLabel('step').getByTitle('reveal attachment').click();
|
||||
await expect(attachment).toBeInViewport();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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.49.0-beta-1731772650000"
|
||||
"@playwright/test": "1.50.0-beta-1737557690000"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.49.0-beta-1731772650000",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-beta-1731772650000.tgz",
|
||||
"integrity": "sha512-0d7DBoGZ23lv1/EkNoFXj5fQ9k3qlYHRE7la68zXihtjTH1DdwEtgdMgXR4UEScF2r/YNXaGRZ7sK/DVu9f6Aw==",
|
||||
"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.49.0-beta-1731772650000"
|
||||
"playwright": "1.50.0-beta-1737557690000"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -38,12 +38,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.49.0-beta-1731772650000",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-beta-1731772650000.tgz",
|
||||
"integrity": "sha512-+LLjx+DMLjx1qiBtLuURTLV3LmFxvQOSaVp9EDMH/qYpclhsp/W41vNxxZEqf8CIsL0BKHIVQYU+6D3OLnJq8g==",
|
||||
"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.49.0-beta-1731772650000"
|
||||
"playwright-core": "1.50.0-beta-1737557690000"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -56,9 +56,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.49.0-beta-1731772650000",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-beta-1731772650000.tgz",
|
||||
"integrity": "sha512-W1HbioibWPPsazFzU/PL9QzGEGubxizQOyMON8/d7DjOpNBqfzuemNuAsNBXucUEVbUlOOzMuoAEX/iqXUOl6Q==",
|
||||
"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.49.0-beta-1731772650000",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-beta-1731772650000.tgz",
|
||||
"integrity": "sha512-0d7DBoGZ23lv1/EkNoFXj5fQ9k3qlYHRE7la68zXihtjTH1DdwEtgdMgXR4UEScF2r/YNXaGRZ7sK/DVu9f6Aw==",
|
||||
"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.49.0-beta-1731772650000"
|
||||
"playwright": "1.50.0-beta-1737557690000"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
|
|
@ -84,18 +84,18 @@
|
|||
"optional": true
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.49.0-beta-1731772650000",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-beta-1731772650000.tgz",
|
||||
"integrity": "sha512-+LLjx+DMLjx1qiBtLuURTLV3LmFxvQOSaVp9EDMH/qYpclhsp/W41vNxxZEqf8CIsL0BKHIVQYU+6D3OLnJq8g==",
|
||||
"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.49.0-beta-1731772650000"
|
||||
"playwright-core": "1.50.0-beta-1737557690000"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.49.0-beta-1731772650000",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-beta-1731772650000.tgz",
|
||||
"integrity": "sha512-W1HbioibWPPsazFzU/PL9QzGEGubxizQOyMON8/d7DjOpNBqfzuemNuAsNBXucUEVbUlOOzMuoAEX/iqXUOl6Q=="
|
||||
"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.49.0-beta-1731772650000"
|
||||
"@playwright/test": "1.50.0-beta-1737557690000"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ test('step timeout option', async ({ runInlineTest }) => {
|
|||
}, { reporter: '', workers: 1 });
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.output).toContain('Error: Step timeout 100ms exceeded.');
|
||||
expect(result.output).toContain('Error: Step timeout of 100ms exceeded.');
|
||||
});
|
||||
|
||||
test('step timeout longer than test timeout', async ({ runInlineTest }) => {
|
||||
|
|
@ -422,6 +422,27 @@ test('step timeout longer than test timeout', async ({ runInlineTest }) => {
|
|||
expect(result.output).toContain('Test timeout of 900ms exceeded.');
|
||||
});
|
||||
|
||||
test('step timeout includes interrupted action errors', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('step with timeout', async ({ page }) => {
|
||||
await test.step('my step', async () => {
|
||||
await page.waitForTimeout(100_000);
|
||||
}, { timeout: 1000 });
|
||||
});
|
||||
`
|
||||
}, { reporter: '', workers: 1 });
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
// Should include 2 errors, one for the step timeout and one for the aborted action.
|
||||
expect.soft(result.output).toContain('TimeoutError: Step timeout of 1000ms exceeded.');
|
||||
expect.soft(result.output).toContain(`> 4 | await test.step('my step', async () => {`);
|
||||
expect.soft(result.output).toContain('Error: page.waitForTimeout: Test ended.');
|
||||
expect.soft(result.output.split('Error: page.waitForTimeout: Test ended.').length).toBe(2);
|
||||
expect.soft(result.output).toContain('> 5 | await page.waitForTimeout(100_000);');
|
||||
});
|
||||
|
||||
test('step timeout is errors.TimeoutError', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ test('should traverse up/down', async ({ runUITest }) => {
|
|||
- treeitem "[icon-circle-outline] fails"
|
||||
- treeitem "[icon-circle-outline] suite" [expanded=false]
|
||||
`);
|
||||
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot();
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await expect.poll(dumpTestTree(page)).toContain(`
|
||||
|
|
@ -180,6 +181,7 @@ test('should traverse up/down', async ({ runUITest }) => {
|
|||
- treeitem "[icon-circle-outline] fails" [selected]
|
||||
- treeitem "[icon-circle-outline] suite" [expanded=false]
|
||||
`);
|
||||
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot();
|
||||
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await expect.poll(dumpTestTree(page)).toContain(`
|
||||
|
|
@ -196,6 +198,7 @@ test('should traverse up/down', async ({ runUITest }) => {
|
|||
- treeitem "[icon-circle-outline] fails"
|
||||
- treeitem "[icon-circle-outline] suite" [expanded=false]
|
||||
`);
|
||||
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot();
|
||||
});
|
||||
|
||||
test('should expand / collapse groups', async ({ runUITest }) => {
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in a new issue