Merge branch 'microsoft:main' into fix/gracefully-close-unused-workers

This commit is contained in:
Noam Gaash 2024-04-25 11:06:18 +03:00 committed by GitHub
commit e4d41a0d59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 1775 additions and 1163 deletions

View file

@ -7,8 +7,6 @@ test/assets/modernizr.js
/packages/playwright-core/types/* /packages/playwright-core/types/*
/packages/playwright-ct-core/src/generated/* /packages/playwright-ct-core/src/generated/*
/index.d.ts /index.d.ts
utils/generate_types/overrides.d.ts
utils/generate_types/test/test.ts
node_modules/ node_modules/
browser_patches/*/checkout/ browser_patches/*/checkout/
browser_patches/chromium/output/ browser_patches/chromium/output/

View file

@ -34,7 +34,7 @@ runs:
artifact_id: artifact.id, artifact_id: artifact.id,
archive_format: 'zip' archive_format: 'zip'
}); });
console.log('downloaded artifact', result); console.log(`Downloaded ${artifact.name}.zip (${result.data.byteLength} bytes)`);
fs.writeFileSync(`${{ inputs.path }}/artifacts/${artifact.name}.zip`, Buffer.from(result.data)); fs.writeFileSync(`${{ inputs.path }}/artifacts/${artifact.name}.zip`, Buffer.from(result.data));
} }
- name: Unzip artifacts - name: Unzip artifacts

View file

@ -1,26 +0,0 @@
name: 'Download blob report from Azure'
description: 'Download blob report from Azure blob storage'
inputs:
blob_prefix:
description: 'Name of the Azure blob storage directory containing blob report'
required: true
output_dir:
description: 'Output directory where downloaded blobs will be stored'
required: true
default: 'blob-report'
connection_string:
description: 'Azure connection string'
required: true
runs:
using: "composite"
steps:
- name: Download Blob Reports from Azure Blob Storage
shell: bash
run: |
OUTPUT_DIR='${{ inputs.output_dir }}'
mkdir -p $OUTPUT_DIR
LIST=$(az storage blob list -c '$web' --prefix ${{ inputs.blob_prefix }} --connection-string "${{ inputs.connection_string }}")
for name in $(echo $LIST | jq --raw-output '.[].name | select(test("report-.*\\.zip$"))');
do
az storage blob download -c '$web' --name $name -f $OUTPUT_DIR/$(basename $name) --connection-string "${{ inputs.connection_string }}"
done

View file

@ -1,7 +0,0 @@
{
"name": "playwright-chromium",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "playwright-core",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "playwright-firefox",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "@playwright/test",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "playwright-webkit",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "playwright",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ node_modules/
*.swp *.swp
*.pyc *.pyc
.vscode .vscode
.mono
.idea .idea
yarn.lock yarn.lock
/packages/playwright-core/src/generated /packages/playwright-core/src/generated

View file

@ -290,9 +290,7 @@ Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` o
**Usage** **Usage**
```js ```js
page.on('dialog', dialog => { page.on('dialog', dialog => dialog.accept());
dialog.accept();
});
``` ```
```java ```java
@ -3157,6 +3155,7 @@ This method lets you set up a special function, called a handler, that activates
Things to keep in mind: Things to keep in mind:
* When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as a part of your normal test flow, instead of using [`method: Page.addLocatorHandler`]. * When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as a part of your normal test flow, instead of using [`method: Page.addLocatorHandler`].
* Playwright checks for the overlay every time before executing or retrying an action that requires an [actionability check](../actionability.md), or before performing an auto-waiting assertion check. When overlay is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't perform any actions, the handler will not be triggered. * Playwright checks for the overlay every time before executing or retrying an action that requires an [actionability check](../actionability.md), or before performing an auto-waiting assertion check. When overlay is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't perform any actions, the handler will not be triggered.
* After executing the handler, Playwright will ensure that overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with [`option: allowStayingVisible`].
* The execution time of the handler counts towards the timeout of the action/assertion that executed the handler. If your handler takes too long, it might cause timeouts. * The execution time of the handler counts towards the timeout of the action/assertion that executed the handler. If your handler takes too long, it might cause timeouts.
* You can register multiple handlers. However, only a single handler will be running at a time. Make sure the actions within a handler don't depend on another handler. * You can register multiple handlers. However, only a single handler will be running at a time. Make sure the actions within a handler don't depend on another handler.
@ -3286,13 +3285,13 @@ await page.GotoAsync("https://example.com");
await page.GetByRole("button", new() { Name = "Start here" }).ClickAsync(); await page.GetByRole("button", new() { Name = "Start here" }).ClickAsync();
``` ```
An example with a custom callback on every actionability check. It uses a `<body>` locator that is always visible, so the handler is called before every actionability check: An example with a custom callback on every actionability check. It uses a `<body>` locator that is always visible, so the handler is called before every actionability check. It is important to specify [`option: allowStayingVisible`], because the handler does not hide the `<body>` element.
```js ```js
// Setup the handler. // Setup the handler.
await page.addLocatorHandler(page.locator('body'), async () => { await page.addLocatorHandler(page.locator('body'), async () => {
await page.evaluate(() => window.removeObstructionsForTestIfNeeded()); await page.evaluate(() => window.removeObstructionsForTestIfNeeded());
}); }, { allowStayingVisible: true });
// Write the test as usual. // Write the test as usual.
await page.goto('https://example.com'); await page.goto('https://example.com');
@ -3303,7 +3302,7 @@ await page.getByRole('button', { name: 'Start here' }).click();
// Setup the handler. // Setup the handler.
page.addLocatorHandler(page.locator("body")), () => { page.addLocatorHandler(page.locator("body")), () => {
page.evaluate("window.removeObstructionsForTestIfNeeded()"); page.evaluate("window.removeObstructionsForTestIfNeeded()");
}); }, new Page.AddLocatorHandlerOptions.setAllowStayingVisible(true));
// Write the test as usual. // Write the test as usual.
page.goto("https://example.com"); page.goto("https://example.com");
@ -3314,7 +3313,7 @@ page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
# Setup the handler. # Setup the handler.
def handler(): def handler():
page.evaluate("window.removeObstructionsForTestIfNeeded()") page.evaluate("window.removeObstructionsForTestIfNeeded()")
page.add_locator_handler(page.locator("body"), handler) page.add_locator_handler(page.locator("body"), handler, allow_staying_visible=True)
# Write the test as usual. # Write the test as usual.
page.goto("https://example.com") page.goto("https://example.com")
@ -3325,7 +3324,7 @@ page.get_by_role("button", name="Start here").click()
# Setup the handler. # Setup the handler.
def handler(): def handler():
await page.evaluate("window.removeObstructionsForTestIfNeeded()") await page.evaluate("window.removeObstructionsForTestIfNeeded()")
await page.add_locator_handler(page.locator("body"), handler) await page.add_locator_handler(page.locator("body"), handler, allow_staying_visible=True)
# Write the test as usual. # Write the test as usual.
await page.goto("https://example.com") await page.goto("https://example.com")
@ -3336,13 +3335,45 @@ await page.get_by_role("button", name="Start here").click()
// Setup the handler. // Setup the handler.
await page.AddLocatorHandlerAsync(page.Locator("body"), async () => { await page.AddLocatorHandlerAsync(page.Locator("body"), async () => {
await page.EvaluateAsync("window.removeObstructionsForTestIfNeeded()"); await page.EvaluateAsync("window.removeObstructionsForTestIfNeeded()");
}); }, new() { AllowStayingVisible = true });
// Write the test as usual. // Write the test as usual.
await page.GotoAsync("https://example.com"); await page.GotoAsync("https://example.com");
await page.GetByRole("button", new() { Name = "Start here" }).ClickAsync(); await page.GetByRole("button", new() { Name = "Start here" }).ClickAsync();
``` ```
Handler takes the original locator as an argument. You can also automatically remove the handler after a number of invocations by setting [`option: times`]:
```js
await page.addLocatorHandler(page.getByLabel('Close'), async locator => {
await locator.click();
}, { times: 1 });
```
```java
page.addLocatorHandler(page.getByLabel("Close"), locator => {
locator.click();
}, new Page.AddLocatorHandlerOptions().setTimes(1));
```
```python sync
def handler(locator):
locator.click()
page.add_locator_handler(page.get_by_label("Close"), handler, times=1)
```
```python async
def handler(locator):
await locator.click()
await page.add_locator_handler(page.get_by_label("Close"), handler, times=1)
```
```csharp
await page.AddLocatorHandlerAsync(page.GetByText("Sign up to the newsletter"), async locator => {
await locator.ClickAsync();
}, new() { Times = 1 });
```
### param: Page.addLocatorHandler.locator ### param: Page.addLocatorHandler.locator
* since: v1.42 * since: v1.42
- `locator` <[Locator]> - `locator` <[Locator]>
@ -3352,24 +3383,67 @@ Locator that triggers the handler.
### param: Page.addLocatorHandler.handler ### param: Page.addLocatorHandler.handler
* langs: js, python * langs: js, python
* since: v1.42 * since: v1.42
- `handler` <[function]> - `handler` <[function]\([Locator]\): [Promise<any>]>
Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click. Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click.
### param: Page.addLocatorHandler.handler ### param: Page.addLocatorHandler.handler
* langs: csharp * langs: csharp
* since: v1.42 * since: v1.42
- `handler` <[function](): [Promise<any>]> - `handler` <[function]\([Locator]\)>
Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click. Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click.
### param: Page.addLocatorHandler.handler ### param: Page.addLocatorHandler.handler
* langs: java * langs: java
* since: v1.42 * since: v1.42
- `handler` <[Runnable]> - `handler` <[function]\([Locator]\)>
Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click. Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click.
### option: Page.addLocatorHandler.times
* since: v1.44
- `times` <[int]>
Specifies the maximum number of times this handler should be called. Unlimited by default.
### option: Page.addLocatorHandler.allowStayingVisible
* since: v1.44
- `allowStayingVisible` <[boolean]>
By default, after calling the handler Playwright will wait until the overlay becomes hidden, and only then Playwright will continue with the action/assertion that triggered the handler. This option allows to opt-out of this behavior, so that overlay can stay visible after the handler has run.
## async method: Page.removeLocatorHandler
* since: v1.44
:::warning[Experimental]
This method is experimental and its behavior may change in the upcoming releases.
:::
Removes locator handler added by [`method: Page.addLocatorHandler`].
### param: Page.removeLocatorHandler.locator
* since: v1.44
- `locator` <[Locator]>
Locator passed to [`method: Page.addLocatorHandler`].
### param: Page.removeLocatorHandler.handler
* langs: js, python
* since: v1.44
- `handler` <[function]\([Locator]\): [Promise<any>]>
Handler passed to [`method: Page.addLocatorHandler`].
### param: Page.addLocatorHandler.handler
* langs: csharp, java
* since: v1.44
- `handler` <[function]\([Locator]\)>
Handler passed to [`method: Page.addLocatorHandler`].
## async method: Page.reload ## async method: Page.reload
* since: v1.8 * since: v1.8
- returns: <[null]|[Response]> - returns: <[null]|[Response]>

View file

@ -1731,13 +1731,17 @@ test.describe('suite', () => {
The list of supported tokens: The list of supported tokens:
* `{testDir}` - Project's [`property: TestConfig.testDir`]. * `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated snapshot name.
* Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with config) * Value: `foo/bar/baz`
* `{snapshotDir}` - Project's [`property: TestConfig.snapshotDir`]. * `{ext}` - snapshot extension (with dots)
* Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`) * Value: `.png`
* `{platform}` - The value of `process.platform`. * `{platform}` - The value of `process.platform`.
* `{projectName}` - Project's file-system-sanitized name, if any. * `{projectName}` - Project's file-system-sanitized name, if any.
* Value: `''` (empty string). * Value: `''` (empty string).
* `{snapshotDir}` - Project's [`property: TestConfig.snapshotDir`].
* Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* `{testDir}` - Project's [`property: TestConfig.testDir`].
* Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with config)
* `{testFileDir}` - Directories in relative path from `testDir` to **test file**. * `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* Value: `page` * Value: `page`
* `{testFileName}` - Test file name with extension. * `{testFileName}` - Test file name with extension.
@ -1746,10 +1750,6 @@ The list of supported tokens:
* Value: `page/page-click.spec.ts` * Value: `page/page-click.spec.ts`
* `{testName}` - File-system-sanitized test title, including parent describes but excluding file name. * `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* Value: `suite-test-should-work` * Value: `suite-test-should-work`
* `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated snapshot name.
* Value: `foo/bar/baz`
* `{ext}` - snapshot extension (with dots)
* Value: `.png`
Each token can be preceded with a single character that will be used **only if** this token has non-empty value. Each token can be preceded with a single character that will be used **only if** this token has non-empty value.

View file

@ -1,135 +0,0 @@
# class: ConfigInWorker
* since: v1.10
* langs: js
Resolved configuration available via [`property: TestInfo.config`] and [`property: WorkerInfo.config`].
## property: ConfigInWorker.configFile
* since: v1.20
- type: ?<[string]>
Path to the configuration file (if any) used to run the tests.
## property: ConfigInWorker.forbidOnly
* since: v1.10
- type: <[boolean]>
See [`property: TestConfig.forbidOnly`].
## property: ConfigInWorker.fullyParallel
* since: v1.20
- type: <[boolean]>
See [`property: TestConfig.fullyParallel`].
## property: ConfigInWorker.globalSetup
* since: v1.10
- type: <[null]|[string]>
See [`property: TestConfig.globalSetup`].
## property: ConfigInWorker.globalTeardown
* since: v1.10
- type: <[null]|[string]>
See [`property: TestConfig.globalTeardown`].
## property: ConfigInWorker.globalTimeout
* since: v1.10
- type: <[int]>
See [`property: TestConfig.globalTimeout`].
## property: ConfigInWorker.grep
* since: v1.10
- type: <[RegExp]|[Array]<[RegExp]>>
See [`property: TestConfig.grep`].
## property: ConfigInWorker.grepInvert
* since: v1.10
- type: <[null]|[RegExp]|[Array]<[RegExp]>>
See [`property: TestConfig.grepInvert`].
## property: ConfigInWorker.maxFailures
* since: v1.10
- type: <[int]>
See [`property: TestConfig.maxFailures`].
## property: ConfigInWorker.metadata
* since: v1.10
- type: <[Metadata]>
See [`property: TestConfig.metadata`].
## property: ConfigInWorker.preserveOutput
* since: v1.10
- type: <[PreserveOutput]<"always"|"never"|"failures-only">>
See [`property: TestConfig.preserveOutput`].
## property: ConfigInWorker.projects
* since: v1.10
- type: <[Array]<[ProjectInWorker]>>
List of resolved projects.
## property: ConfigInWorker.quiet
* since: v1.10
- type: <[boolean]>
See [`property: TestConfig.quiet`].
## property: ConfigInWorker.reporter
* since: v1.10
- type: <[string]|[Array]<[Object]>|[BuiltInReporter]<"list"|"dot"|"line"|"github"|"json"|"junit"|"null"|"html">>
- `0` <[string]> Reporter name or module or file path
- `1` <[Object]> An object with reporter options if any
See [`property: TestConfig.reporter`].
## property: ConfigInWorker.reportSlowTests
* since: v1.10
- type: <[null]|[Object]>
- `max` <[int]> The maximum number of slow test files to report. Defaults to `5`.
- `threshold` <[float]> Test duration in milliseconds that is considered slow. Defaults to 15 seconds.
See [`property: TestConfig.reportSlowTests`].
## property: ConfigInWorker.rootDir
* since: v1.20
- type: <[string]>
## property: ConfigInWorker.shard
* since: v1.10
- type: <[null]|[Object]>
- `total` <[int]> The total number of shards.
- `current` <[int]> The index of the shard to execute, one-based.
See [`property: TestConfig.shard`].
## property: ConfigInWorker.updateSnapshots
* since: v1.10
- type: <[UpdateSnapshots]<"all"|"none"|"missing">>
See [`property: TestConfig.updateSnapshots`].
## property: ConfigInWorker.version
* since: v1.20
- type: <[string]>
Playwright version.
## property: ConfigInWorker.webServer
* since: v1.10
- type: <[null]|[Object]>
See [`property: TestConfig.webServer`].
## property: ConfigInWorker.workers
* since: v1.10
- type: <[int]>
See [`property: TestConfig.workers`].

View file

@ -2,13 +2,13 @@
* since: v1.10 * since: v1.10
* langs: js * langs: js
Resolved configuration passed to [`method: Reporter.onBegin`]. Resolved configuration which is accessible via [`property: TestInfo.config`] and is passed to the test reporters. To see the format of Playwright configuration file, please see [TestConfig] instead.
## property: FullConfig.configFile ## property: FullConfig.configFile
* since: v1.20 * since: v1.20
- type: ?<[string]> - type: ?<[string]>
Path to the configuration file (if any) used to run the tests. Path to the configuration file used to run the tests. The value is an empty string if no config file was used.
## property: FullConfig.forbidOnly ## property: FullConfig.forbidOnly
* since: v1.10 * since: v1.10
@ -102,6 +102,8 @@ See [`property: TestConfig.reportSlowTests`].
* since: v1.20 * since: v1.20
- type: <[string]> - type: <[string]>
Base directory for all relative paths used in the reporters.
## property: FullConfig.shard ## property: FullConfig.shard
* since: v1.10 * since: v1.10
- type: <[null]|[Object]> - type: <[null]|[Object]>

View file

@ -2,10 +2,7 @@
* since: v1.10 * since: v1.10
* langs: js * langs: js
Runtime representation of the test project configuration that is passed Runtime representation of the test project configuration. It is accessible in the tests via [`property: TestInfo.project`] and [`property: WorkerInfo.project`] and is passed to the test reporters. To see the format of the project in the Playwright configuration file please see [TestProject] instead.
to [Reporter]. It exposes some of the resolved fields declared in
[TestProject]. You can get [FullProject] instance from [`property: FullConfig.projects`]
or [`method: Suite.project`].
## property: FullProject.dependencies ## property: FullProject.dependencies
* since: v1.31 * since: v1.31

View file

@ -1,96 +0,0 @@
# class: ProjectInWorker
* since: v1.10
* langs: js
Runtime representation of the test project configuration that can be accessed
in the tests via [`property: TestInfo.project`] and [`property: WorkerInfo.project`].
## property: ProjectInWorker.dependencies
* since: v1.31
- type: <[Array]<[string]>>
See [`property: TestProject.dependencies`].
## property: ProjectInWorker.grep
* since: v1.10
- type: <[RegExp]|[Array]<[RegExp]>>
See [`property: TestProject.grep`].
## property: ProjectInWorker.grepInvert
* since: v1.10
- type: <[null]|[RegExp]|[Array]<[RegExp]>>
See [`property: TestProject.grepInvert`].
## property: ProjectInWorker.metadata
* since: v1.10
- type: <[Metadata]>
See [`property: TestProject.metadata`].
## property: ProjectInWorker.name
* since: v1.10
- type: <[string]>
See [`property: TestProject.name`].
## property: ProjectInWorker.snapshotDir
* since: v1.10
- type: <[string]>
See [`property: TestProject.snapshotDir`].
## property: ProjectInWorker.outputDir
* since: v1.10
- type: <[string]>
See [`property: TestProject.outputDir`].
## property: ProjectInWorker.repeatEach
* since: v1.10
- type: <[int]>
See [`property: TestProject.repeatEach`].
## property: ProjectInWorker.retries
* since: v1.10
- type: <[int]>
See [`property: TestProject.retries`].
## property: ProjectInWorker.teardown
* since: v1.34
- type: ?<[string]>
See [`property: TestProject.teardown`].
## property: ProjectInWorker.testDir
* since: v1.10
- type: <[string]>
See [`property: TestProject.testDir`].
## property: ProjectInWorker.testIgnore
* since: v1.10
- type: <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
See [`property: TestProject.testIgnore`].
## property: ProjectInWorker.testMatch
* since: v1.10
- type: <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
See [`property: TestProject.testMatch`].
## property: ProjectInWorker.timeout
* since: v1.10
- type: <[int]>
See [`property: TestProject.timeout`].
## property: ProjectInWorker.use
* since: v1.10
- type: <[Fixtures]>
See [`property: TestProject.use`].

View file

@ -2,7 +2,7 @@
* since: v1.10 * since: v1.10
* langs: js * langs: js
Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or `testDir`. These options are described in the [TestConfig] object in the [configuration file](../test-configuration.md). Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or `testDir`. These options are described in the [TestConfig] object in the [configuration file](../test-configuration.md). This type describes format of the configuration file, to access resolved configuration parameters at run time use [FullConfig].
Playwright Test supports running multiple test projects at the same time. Project-specific options should be put to [`property: TestConfig.projects`], but top-level [TestConfig] can also define base options shared between all projects. Playwright Test supports running multiple test projects at the same time. Project-specific options should be put to [`property: TestConfig.projects`], but top-level [TestConfig] can also define base options shared between all projects.
@ -41,20 +41,20 @@ export default defineConfig({
- type: ?<[Object]> - type: ?<[Object]>
- `timeout` ?<[int]> Default timeout for async expect matchers in milliseconds, defaults to 5000ms. - `timeout` ?<[int]> Default timeout for async expect matchers in milliseconds, defaults to 5000ms.
- `toHaveScreenshot` ?<[Object]> Configuration for the [`method: PageAssertions.toHaveScreenshot#1`] method. - `toHaveScreenshot` ?<[Object]> Configuration for the [`method: PageAssertions.toHaveScreenshot#1`] method.
- `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default.
- `maxDiffPixelRatio` ?<[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
- `animations` ?<[ScreenshotAnimations]<"allow"|"disabled">> See [`option: animations`] in [`method: Page.screenshot`]. Defaults to `"disabled"`. - `animations` ?<[ScreenshotAnimations]<"allow"|"disabled">> See [`option: animations`] in [`method: Page.screenshot`]. Defaults to `"disabled"`.
- `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`. - `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`.
- `maxDiffPixels` ?<[int]> An acceptable amount of pixels that could be different, unset by default.
- `maxDiffPixelRatio` ?<[float]> An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: scale`] in [`method: Page.screenshot`]. Defaults to `"css"`. - `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
- `stylePath` ?<[string]|[Array]<[string]>> See [`option: style`] in [`method: Page.screenshot`]. - `stylePath` ?<[string]|[Array]<[string]>> See [`option: style`] in [`method: Page.screenshot`].
- `threshold` ?<[float]> An acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method. - `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
- `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`. - `maxDiffPixels` ?<[int]> An acceptable amount of pixels that could be different, unset by default.
- `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default. - `maxDiffPixelRatio` ?<[float]> An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
- `maxDiffPixelRatio` ?<[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default. - `threshold` ?<[float]> An acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `toPass` ?<[Object]> Configuration for the [expect(value).toPass()](../test-assertions.md#expecttopass) method. - `toPass` ?<[Object]> Configuration for the [expect(value).toPass()](../test-assertions.md#expecttopass) method.
- `timeout` ?<[int]> timeout for toPass method in milliseconds. - `intervals` ?<[Array]<[int]>> Probe intervals for toPass method in milliseconds.
- `intervals` ?<[Array]<[int]>> probe intervals for toPass method in milliseconds. - `timeout` ?<[int]> Timeout for toPass method in milliseconds.
Configuration for the `expect` assertion library. Learn more about [various timeouts](../test-timeouts.md). Configuration for the `expect` assertion library. Learn more about [various timeouts](../test-timeouts.md).
@ -112,7 +112,7 @@ export default defineConfig({
* since: v1.10 * since: v1.10
- type: ?<[string]> - type: ?<[string]>
Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [`TestConfig`] argument. Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [FullConfig] argument.
Learn more about [global setup and teardown](../test-global-setup-teardown.md). Learn more about [global setup and teardown](../test-global-setup-teardown.md).
@ -458,8 +458,8 @@ export default defineConfig({
## property: TestConfig.shard ## property: TestConfig.shard
* since: v1.10 * since: v1.10
- type: ?<[null]|[Object]> - type: ?<[null]|[Object]>
- `total` <[int]> The total number of shards.
- `current` <[int]> The index of the shard to execute, one-based. - `current` <[int]> The index of the shard to execute, one-based.
- `total` <[int]> The total number of shards.
Shard tests and execute only the selected shard. Specify in the one-based form like `{ total: 5, current: 2 }`. Shard tests and execute only the selected shard. Specify in the one-based form like `{ total: 5, current: 2 }`.
@ -589,15 +589,15 @@ export default defineConfig({
* since: v1.10 * since: v1.10
- type: ?<[Object]|[Array]<[Object]>> - type: ?<[Object]|[Array]<[Object]>>
- `command` <[string]> Shell command to start. For example `npm run start`.. - `command` <[string]> Shell command to start. For example `npm run start`..
- `port` ?<[int]> The port that your http server is expected to appear on. It does wait until it accepts connections. Either `port` or `url` should be specified. - `cwd` ?<[string]> Current working directory of the spawned process, defaults to the directory of the configuration file.
- `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. - `env` ?<[Object]<[string], [string]>> Environment variables to set for the command, `process.env` by default.
- `ignoreHTTPSErrors` ?<[boolean]> Whether to ignore HTTPS errors when fetching the `url`. Defaults to `false`. - `ignoreHTTPSErrors` ?<[boolean]> Whether to ignore HTTPS errors when fetching the `url`. Defaults to `false`.
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. - `port` ?<[int]> The port that your http server is expected to appear on. It does wait until it accepts connections. Either `port` or `url` should be specified.
- `reuseExistingServer` ?<[boolean]> If true, it will re-use an existing server on the `port` or `url` when available. If no server is running on that `port` or `url`, it will run the command to start a new server. If `false`, it will throw if an existing process is listening on the `port` or `url`. This should be commonly set to `!process.env.CI` to allow the local dev server when running tests locally. - `reuseExistingServer` ?<[boolean]> If true, it will re-use an existing server on the `port` or `url` when available. If no server is running on that `port` or `url`, it will run the command to start a new server. If `false`, it will throw if an existing process is listening on the `port` or `url`. This should be commonly set to `!process.env.CI` to allow the local dev server when running tests locally.
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. - `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. - `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
- `cwd` ?<[string]> Current working directory of the spawned process, defaults to the directory of the configuration file. - `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
- `env` ?<[Object]<[string], [string]>> Environment variables to set for the command, `process.env` by default. - `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.
Launch a development web server (or multiple) during the tests. Launch a development web server (or multiple) during the tests.

View file

@ -106,7 +106,7 @@ Column number where the currently running test is declared.
## property: TestInfo.config ## property: TestInfo.config
* since: v1.10 * since: v1.10
- type: <[ConfigInWorker]> - type: <[FullConfig]>
Processed configuration from the [configuration file](../test-configuration.md). Processed configuration from the [configuration file](../test-configuration.md).
@ -279,7 +279,7 @@ Also available as `process.env.TEST_PARALLEL_INDEX`. Learn more about [paralleli
## property: TestInfo.project ## property: TestInfo.project
* since: v1.10 * since: v1.10
- type: <[ProjectInWorker]> - type: <[FullProject]>
Processed project configuration from the [configuration file](../test-configuration.md). Processed project configuration from the [configuration file](../test-configuration.md).

View file

@ -2,7 +2,7 @@
* since: v1.10 * since: v1.10
* langs: js * langs: js
Playwright Test supports running multiple test projects at the same time. This is useful for running tests in multiple configurations. For example, consider running tests against multiple browsers. Playwright Test supports running multiple test projects at the same time. This is useful for running tests in multiple configurations. For example, consider running tests against multiple browsers. This type describes format of a project in the configuration file, to access resolved configuration parameters at run time use [FullProject].
`TestProject` encapsulates configuration specific to a single project. Projects are configured in [`property: TestConfig.projects`] specified in the [configuration file](../test-configuration.md). Note that all properties of [TestProject] are available in the top-level [TestConfig], in which case they are shared between all projects. `TestProject` encapsulates configuration specific to a single project. Projects are configured in [`property: TestConfig.projects`] specified in the [configuration file](../test-configuration.md). Note that all properties of [TestProject] are available in the top-level [TestConfig], in which case they are shared between all projects.

View file

@ -6,7 +6,7 @@
## property: WorkerInfo.config ## property: WorkerInfo.config
* since: v1.10 * since: v1.10
- type: <[ConfigInWorker]> - type: <[FullConfig]>
Processed configuration from the [configuration file](../test-configuration.md). Processed configuration from the [configuration file](../test-configuration.md).
@ -22,7 +22,7 @@ Also available as `process.env.TEST_PARALLEL_INDEX`. Learn more about [paralleli
## property: WorkerInfo.project ## property: WorkerInfo.project
* since: v1.10 * since: v1.10
- type: <[ProjectInWorker]> - type: <[FullProject]>
Processed project configuration from the [configuration file](../test-configuration.md). Processed project configuration from the [configuration file](../test-configuration.md).

View file

@ -30,7 +30,7 @@ Returns the list of all test cases in this suite and its descendants, as opposit
* since: v1.44 * since: v1.44
- type: <[Array]<[TestCase]|[Suite]>> - type: <[Array]<[TestCase]|[Suite]>>
Test cases and suites defined directly in this suite. The elements are returned in their declaration order. You can discriminate between different entry types using [`property: TestCase.type`] and [`property: Suite.type`]. Test cases and suites defined directly in this suite. The elements are returned in their declaration order. You can differentiate between various entry types by using [`property: TestCase.type`] and [`property: Suite.type`].
## property: Suite.location ## property: Suite.location
* since: v1.10 * since: v1.10

View file

@ -112,4 +112,4 @@ Returns a list of titles from the root down to this test.
* since: v1.44 * since: v1.44
- returns: <[TestCaseType]<"test">> - returns: <[TestCaseType]<"test">>
Returns type of the test. Returns "test". Useful for detecting test cases in [`method: Suite.entries`].

View file

@ -55,6 +55,7 @@ def test_my_app_is_working(fixture_name):
- `context`: New [browser context](./browser-contexts) for a test. - `context`: New [browser context](./browser-contexts) for a test.
- `page`: New [browser page](./pages) for a test. - `page`: New [browser page](./pages) for a test.
- `new_context`: Allows creating different [browser contexts](./browser-contexts) for a test. Useful for multi-user scenarios. Accepts the same parameters as [`method: Browser.newContext`].
**Session scope**: These fixtures are created when requested in a test function and destroyed when all tests end. **Session scope**: These fixtures are created when requested in a test function and destroyed when all tests end.
@ -211,30 +212,6 @@ def browser_context_args(browser_context_args, playwright):
Or via the CLI `--device="iPhone 11 Pro"` Or via the CLI `--device="iPhone 11 Pro"`
### Persistent context
```py title="conftest.py"
import pytest
from playwright.sync_api import BrowserType
from typing import Dict
@pytest.fixture(scope="session")
def context(
browser_type: BrowserType,
browser_type_launch_args: Dict,
browser_context_args: Dict
):
context = browser_type.launch_persistent_context("./foobar", **{
**browser_type_launch_args,
**browser_context_args,
"locale": "de-DE",
})
yield context
context.close()
```
When using that all pages inside your test are created from the persistent context.
### Using with `unittest.TestCase` ### Using with `unittest.TestCase`
See the following example for using it with `unittest.TestCase`. This has a limitation, See the following example for using it with `unittest.TestCase`. This has a limitation,

View file

@ -27,13 +27,13 @@
}, },
{ {
"name": "firefox-beta", "name": "firefox-beta",
"revision": "1447", "revision": "1449",
"installByDefault": false, "installByDefault": false,
"browserVersion": "125.0b3" "browserVersion": "126.0b1"
}, },
{ {
"name": "webkit", "name": "webkit",
"revision": "2001", "revision": "2002",
"installByDefault": true, "installByDefault": true,
"revisionOverrides": { "revisionOverrides": {
"mac10.14": "1446", "mac10.14": "1446",

View file

@ -80,6 +80,10 @@ export class Locator implements api.Locator {
}); });
} }
_equals(locator: Locator) {
return this._frame === locator._frame && this._selector === locator._selector;
}
page() { page() {
return this._frame.page(); return this._frame.page();
} }

View file

@ -96,7 +96,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
_closeWasCalled: boolean = false; _closeWasCalled: boolean = false;
private _harRouters: HarRouter[] = []; private _harRouters: HarRouter[] = [];
private _locatorHandlers = new Map<number, Function>(); private _locatorHandlers = new Map<number, { locator: Locator, handler: (locator: Locator) => any, times: number | undefined }>();
static from(page: channels.PageChannel): Page { static from(page: channels.PageChannel): Page {
return (page as any)._object; return (page as any)._object;
@ -362,19 +362,36 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return Response.fromNullable((await this._channel.reload({ ...options, waitUntil })).response); return Response.fromNullable((await this._channel.reload({ ...options, waitUntil })).response);
} }
async addLocatorHandler(locator: Locator, handler: Function): Promise<void> { async addLocatorHandler(locator: Locator, handler: (locator: Locator) => any, options: { times?: number, allowStayingVisible?: boolean } = {}): Promise<void> {
if (locator._frame !== this._mainFrame) if (locator._frame !== this._mainFrame)
throw new Error(`Locator must belong to the main frame of this page`); throw new Error(`Locator must belong to the main frame of this page`);
const { uid } = await this._channel.registerLocatorHandler({ selector: locator._selector }); if (options.times === 0)
this._locatorHandlers.set(uid, handler); return;
const { uid } = await this._channel.registerLocatorHandler({ selector: locator._selector, allowStayingVisible: options.allowStayingVisible });
this._locatorHandlers.set(uid, { locator, handler, times: options.times });
} }
private async _onLocatorHandlerTriggered(uid: number) { private async _onLocatorHandlerTriggered(uid: number) {
let remove = false;
try { try {
const handler = this._locatorHandlers.get(uid); const handler = this._locatorHandlers.get(uid);
await handler?.(); if (handler && handler.times !== 0) {
if (handler.times !== undefined)
handler.times--;
await handler.handler(handler.locator);
}
remove = handler?.times === 0;
} finally { } finally {
this._wrapApiCall(() => this._channel.resolveLocatorHandlerNoReply({ uid }), true).catch(() => {}); this._wrapApiCall(() => this._channel.resolveLocatorHandlerNoReply({ uid, remove }), true).catch(() => {});
}
}
async removeLocatorHandler(locator: Locator, handler: (locator: Locator) => any): Promise<void> {
for (const [uid, data] of this._locatorHandlers) {
if (data.locator._equals(locator) && data.handler === handler) {
this._locatorHandlers.delete(uid);
await this._channel.unregisterLocatorHandlerNoReply({ uid }).catch(() => {});
}
} }
} }

View file

@ -1046,14 +1046,20 @@ scheme.PageGoForwardResult = tObject({
}); });
scheme.PageRegisterLocatorHandlerParams = tObject({ scheme.PageRegisterLocatorHandlerParams = tObject({
selector: tString, selector: tString,
allowStayingVisible: tOptional(tBoolean),
}); });
scheme.PageRegisterLocatorHandlerResult = tObject({ scheme.PageRegisterLocatorHandlerResult = tObject({
uid: tNumber, uid: tNumber,
}); });
scheme.PageResolveLocatorHandlerNoReplyParams = tObject({ scheme.PageResolveLocatorHandlerNoReplyParams = tObject({
uid: tNumber, uid: tNumber,
remove: tOptional(tBoolean),
}); });
scheme.PageResolveLocatorHandlerNoReplyResult = tOptional(tObject({})); scheme.PageResolveLocatorHandlerNoReplyResult = tOptional(tObject({}));
scheme.PageUnregisterLocatorHandlerNoReplyParams = tObject({
uid: tNumber,
});
scheme.PageUnregisterLocatorHandlerNoReplyResult = tOptional(tObject({}));
scheme.PageReloadParams = tObject({ scheme.PageReloadParams = tObject({
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
waitUntil: tOptional(tType('LifecycleEvent')), waitUntil: tOptional(tType('LifecycleEvent')),

View file

@ -138,12 +138,16 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
} }
async registerLocatorHandler(params: channels.PageRegisterLocatorHandlerParams, metadata: CallMetadata): Promise<channels.PageRegisterLocatorHandlerResult> { async registerLocatorHandler(params: channels.PageRegisterLocatorHandlerParams, metadata: CallMetadata): Promise<channels.PageRegisterLocatorHandlerResult> {
const uid = this._page.registerLocatorHandler(params.selector); const uid = this._page.registerLocatorHandler(params.selector, params.allowStayingVisible);
return { uid }; return { uid };
} }
async resolveLocatorHandlerNoReply(params: channels.PageResolveLocatorHandlerNoReplyParams, metadata: CallMetadata): Promise<void> { async resolveLocatorHandlerNoReply(params: channels.PageResolveLocatorHandlerNoReplyParams, metadata: CallMetadata): Promise<void> {
this._page.resolveLocatorHandler(params.uid); this._page.resolveLocatorHandler(params.uid, params.remove);
}
async unregisterLocatorHandlerNoReply(params: channels.PageUnregisterLocatorHandlerNoReplyParams, metadata: CallMetadata): Promise<void> {
this._page.unregisterLocatorHandler(params.uid);
} }
async emulateMedia(params: channels.PageEmulateMediaParams, metadata: CallMetadata): Promise<void> { async emulateMedia(params: channels.PageEmulateMediaParams, metadata: CallMetadata): Promise<void> {

View file

@ -773,56 +773,61 @@ export class Frame extends SdkObject {
throw new Error(`state: expected one of (attached|detached|visible|hidden)`); throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
return controller.run(async progress => { return controller.run(async progress => {
progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`); progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`);
const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { return await this.waitForSelectorInternal(progress, selector, options, scope);
const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope);
progress.throwIfAborted();
if (!resolved) {
if (state === 'hidden' || state === 'detached')
return null;
return continuePolling;
}
const result = await resolved.injected.evaluateHandle((injected, { info, root }) => {
const elements = injected.querySelectorAll(info.parsed, root || document);
const element: Element | undefined = elements[0];
const visible = element ? injected.isVisible(element) : false;
let log = '';
if (elements.length > 1) {
if (info.strict)
throw injected.strictModeViolationError(info.parsed, elements);
log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`;
} else if (element) {
log = ` locator resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`;
}
return { log, element, visible, attached: !!element };
}, { info: resolved.info, root: resolved.frame === this ? scope : undefined });
const { log, visible, attached } = await result.evaluate(r => ({ log: r.log, visible: r.visible, attached: r.attached }));
if (log)
progress.log(log);
const success = { attached, detached: !attached, visible, hidden: !visible }[state];
if (!success) {
result.dispose();
return continuePolling;
}
if (options.omitReturnValue) {
result.dispose();
return null;
}
const element = state === 'attached' || state === 'visible' ? await result.evaluateHandle(r => r.element) : null;
result.dispose();
if (!element)
return null;
if ((options as any).__testHookBeforeAdoptNode)
await (options as any).__testHookBeforeAdoptNode();
try {
return await element._adoptTo(await resolved.frame._mainContext());
} catch (e) {
return continuePolling;
}
});
return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async waitForSelectorInternal(progress: Progress, selector: string, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
const { state = 'visible' } = options;
const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope);
progress.throwIfAborted();
if (!resolved) {
if (state === 'hidden' || state === 'detached')
return null;
return continuePolling;
}
const result = await resolved.injected.evaluateHandle((injected, { info, root }) => {
const elements = injected.querySelectorAll(info.parsed, root || document);
const element: Element | undefined = elements[0];
const visible = element ? injected.isVisible(element) : false;
let log = '';
if (elements.length > 1) {
if (info.strict)
throw injected.strictModeViolationError(info.parsed, elements);
log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`;
} else if (element) {
log = ` locator resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`;
}
return { log, element, visible, attached: !!element };
}, { info: resolved.info, root: resolved.frame === this ? scope : undefined });
const { log, visible, attached } = await result.evaluate(r => ({ log: r.log, visible: r.visible, attached: r.attached }));
if (log)
progress.log(log);
const success = { attached, detached: !attached, visible, hidden: !visible }[state];
if (!success) {
result.dispose();
return continuePolling;
}
if (options.omitReturnValue) {
result.dispose();
return null;
}
const element = state === 'attached' || state === 'visible' ? await result.evaluateHandle(r => r.element) : null;
result.dispose();
if (!element)
return null;
if ((options as any).__testHookBeforeAdoptNode)
await (options as any).__testHookBeforeAdoptNode();
try {
return await element._adoptTo(await resolved.frame._mainContext());
} catch (e) {
return continuePolling;
}
});
return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
}
async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit: Object = {}, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<void> { async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit: Object = {}, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<void> {
await this._callOnElementOnceMatches(metadata, selector, (injectedScript, element, data) => { await this._callOnElementOnceMatches(metadata, selector, (injectedScript, element, data) => {
injectedScript.dispatchEvent(element, data.type, data.eventInit); injectedScript.dispatchEvent(element, data.type, data.eventInit);

View file

@ -168,7 +168,7 @@ export class Page extends SdkObject {
_video: Artifact | null = null; _video: Artifact | null = null;
_opener: Page | undefined; _opener: Page | undefined;
private _isServerSideOnly = false; private _isServerSideOnly = false;
private _locatorHandlers = new Map<number, { selector: string, resolved?: ManualPromise<void> }>(); private _locatorHandlers = new Map<number, { selector: string, allowStayingVisible?: boolean, resolved?: ManualPromise<void> }>();
private _lastLocatorHandlerUid = 0; private _lastLocatorHandlerUid = 0;
private _locatorHandlerRunningCounter = 0; private _locatorHandlerRunningCounter = 0;
@ -432,20 +432,26 @@ export class Page extends SdkObject {
}), this._timeoutSettings.navigationTimeout(options)); }), this._timeoutSettings.navigationTimeout(options));
} }
registerLocatorHandler(selector: string) { registerLocatorHandler(selector: string, allowStayingVisible: boolean | undefined) {
const uid = ++this._lastLocatorHandlerUid; const uid = ++this._lastLocatorHandlerUid;
this._locatorHandlers.set(uid, { selector }); this._locatorHandlers.set(uid, { selector, allowStayingVisible });
return uid; return uid;
} }
resolveLocatorHandler(uid: number) { resolveLocatorHandler(uid: number, remove: boolean | undefined) {
const handler = this._locatorHandlers.get(uid); const handler = this._locatorHandlers.get(uid);
if (remove)
this._locatorHandlers.delete(uid);
if (handler) { if (handler) {
handler.resolved?.resolve(); handler.resolved?.resolve();
handler.resolved = undefined; handler.resolved = undefined;
} }
} }
unregisterLocatorHandler(uid: number) {
this._locatorHandlers.delete(uid);
}
async performLocatorHandlersCheckpoint(progress: Progress) { async performLocatorHandlersCheckpoint(progress: Progress) {
// Do not run locator handlers from inside locator handler callbacks to avoid deadlocks. // Do not run locator handlers from inside locator handler callbacks to avoid deadlocks.
if (this._locatorHandlerRunningCounter) if (this._locatorHandlerRunningCounter)
@ -460,7 +466,12 @@ export class Page extends SdkObject {
if (handler.resolved) { if (handler.resolved) {
++this._locatorHandlerRunningCounter; ++this._locatorHandlerRunningCounter;
progress.log(` found ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)}, intercepting action to run the handler`); progress.log(` found ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)}, intercepting action to run the handler`);
await this.openScope.race(handler.resolved).finally(() => --this._locatorHandlerRunningCounter); const promise = handler.resolved.then(async () => {
progress.throwIfAborted();
if (!handler.allowStayingVisible)
await this.mainFrame().waitForSelectorInternal(progress, handler.selector, { state: 'hidden' });
});
await this.openScope.race(promise).finally(() => --this._locatorHandlerRunningCounter);
// Avoid side-effects after long-running operation. // Avoid side-effects after long-running operation.
progress.throwIfAborted(); progress.throwIfAborted();
progress.log(` interception handler has finished, continuing`); progress.log(` interception handler has finished, continuing`);

File diff suppressed because it is too large Load diff

View file

@ -183,7 +183,7 @@ export function transformIndexFile(id: string, content: string, templateDir: str
lines.push(registerSource); lines.push(registerSource);
for (const value of importInfos.values()) { for (const value of importInfos.values()) {
const importPath = resolveHook(value.filename, value.importSource); const importPath = resolveHook(value.filename, value.importSource) || value.importSource;
lines.push(`const ${value.id} = () => import('${importPath?.replaceAll(path.sep, '/')}').then((mod) => mod.${value.remoteName || 'default'});`); lines.push(`const ${value.id} = () => import('${importPath?.replaceAll(path.sep, '/')}').then((mod) => mod.${value.remoteName || 'default'});`);
} }

View file

@ -16,6 +16,7 @@
import { chokidar } from './utilsBundle'; import { chokidar } from './utilsBundle';
import type { FSWatcher } from 'chokidar'; import type { FSWatcher } from 'chokidar';
import path from 'path';
export type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string }; export type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string };
@ -51,7 +52,8 @@ export class Watcher {
if (!this._watchedFiles.length) if (!this._watchedFiles.length)
return; return;
this._fsWatcher = chokidar.watch(watchedFiles, { ignoreInitial: true, ignored: this._ignoredFolders }).on('all', async (event, file) => { const ignored = [...this._ignoredFolders, (name: string) => name.includes(path.sep + 'node_modules' + path.sep)];
this._fsWatcher = chokidar.watch(watchedFiles, { ignoreInitial: true, ignored }).on('all', async (event, file) => {
if (this._throttleTimer) if (this._throttleTimer)
clearTimeout(this._throttleTimer); clearTimeout(this._throttleTimer);
if (this._mode === 'flat' && event !== 'add' && event !== 'change') if (this._mode === 'flat' && event !== 'add' && event !== 'change')

View file

@ -124,9 +124,9 @@ export type JsonEvent = {
}; };
type TeleReporterReceiverOptions = { type TeleReporterReceiverOptions = {
mergeProjects: boolean; mergeProjects?: boolean;
mergeTestCases: boolean; mergeTestCases?: boolean;
resolvePath: (rootDir: string, relativePath: string) => string; resolvePath?: (rootDir: string, relativePath: string) => string;
configOverrides?: Pick<reporterTypes.FullConfig, 'configFile' | 'quiet' | 'reportSlowTests' | 'reporter'>; configOverrides?: Pick<reporterTypes.FullConfig, 'configFile' | 'quiet' | 'reportSlowTests' | 'reporter'>;
clearPreviousResultsWhenTestBegins?: boolean; clearPreviousResultsWhenTestBegins?: boolean;
}; };
@ -140,7 +140,7 @@ export class TeleReporterReceiver {
private _rootDir!: string; private _rootDir!: string;
private _config!: reporterTypes.FullConfig; private _config!: reporterTypes.FullConfig;
constructor(reporter: Partial<ReporterV2>, options: TeleReporterReceiverOptions) { constructor(reporter: Partial<ReporterV2>, options: TeleReporterReceiverOptions = {}) {
this._rootSuite = new TeleSuite('', 'root'); this._rootSuite = new TeleSuite('', 'root');
this._options = options; this._options = options;
this._reporter = reporter; this._reporter = reporter;
@ -388,7 +388,7 @@ export class TeleReporterReceiver {
private _absolutePath(relativePath?: string): string | undefined { private _absolutePath(relativePath?: string): string | undefined {
if (relativePath === undefined) if (relativePath === undefined)
return; return;
return this._options.resolvePath(this._rootDir, relativePath); return this._options.resolvePath ? this._options.resolvePath(this._rootDir, relativePath) : this._rootDir + '/' + relativePath;
} }
} }

View file

@ -8,4 +8,5 @@
../util.ts ../util.ts
../utilsBundle.ts ../utilsBundle.ts
../isomorphic/folders.ts ../isomorphic/folders.ts
../isomorphic/teleReceiver.ts
../fsWatcher.ts ../fsWatcher.ts

View file

@ -39,6 +39,7 @@ import type { TraceViewerRedirectOptions, TraceViewerServerOptions } from 'playw
import type { TestRunnerPluginRegistration } from '../plugins'; import type { TestRunnerPluginRegistration } from '../plugins';
import { serializeError } from '../util'; import { serializeError } from '../util';
import { cacheDir } from '../transform/compilationCache'; import { cacheDir } from '../transform/compilationCache';
import { baseFullConfig } from '../isomorphic/teleReceiver';
const originalStdoutWrite = process.stdout.write; const originalStdoutWrite = process.stdout.write;
const originalStderrWrite = process.stderr.write; const originalStderrWrite = process.stderr.write;
@ -147,7 +148,10 @@ class TestServerDispatcher implements TestServerInterface {
const { reporter, report } = await this._collectingReporter(); const { reporter, report } = await this._collectingReporter();
const { config, error } = await this._loadConfig(); const { config, error } = await this._loadConfig();
if (!config) { if (!config) {
// Produce dummy config when it has an error.
reporter.onConfigure(baseFullConfig);
reporter.onError(error!); reporter.onError(error!);
await reporter.onExit();
return { status: 'failed', report }; return { status: 'failed', report };
} }

View file

@ -17,13 +17,13 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
import type { TestInfoError, TestInfo, TestStatus, ProjectInWorker, ConfigInWorker } from '../../types/test'; import type { TestInfoError, TestInfo, TestStatus, FullProject } from '../../types/test';
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc'; import type { AttachmentPayload, StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc';
import type { TestCase } from '../common/test'; import type { TestCase } from '../common/test';
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
import type { RunnableDescription } from './timeoutManager'; import type { RunnableDescription } from './timeoutManager';
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config'; import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
import type { Location } from '../../types/testReporter'; import type { FullConfig, Location } from '../../types/testReporter';
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString, windowsFilesystemFriendlyLength } from '../util'; import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString, windowsFilesystemFriendlyLength } from '../util';
import { TestTracing } from './testTracing'; import { TestTracing } from './testTracing';
import type { Attachment } from './testTracing'; import type { Attachment } from './testTracing';
@ -81,8 +81,8 @@ export class TestInfoImpl implements TestInfo {
readonly retry: number; readonly retry: number;
readonly workerIndex: number; readonly workerIndex: number;
readonly parallelIndex: number; readonly parallelIndex: number;
readonly project: ProjectInWorker; readonly project: FullProject;
readonly config: ConfigInWorker; readonly config: FullConfig;
readonly title: string; readonly title: string;
readonly titlePath: string[]; readonly titlePath: string[];
readonly file: string; readonly file: string;

View file

@ -35,7 +35,9 @@ type UseOptions<TestArgs, WorkerArgs> = Partial<WorkerArgs> & Partial<TestArgs>;
/** /**
* Playwright Test supports running multiple test projects at the same time. This is useful for running tests in * Playwright Test supports running multiple test projects at the same time. This is useful for running tests in
* multiple configurations. For example, consider running tests against multiple browsers. * multiple configurations. For example, consider running tests against multiple browsers. This type describes format
* of a project in the configuration file, to access resolved configuration parameters at run time use {@link
* FullProject}.
* *
* `TestProject` encapsulates configuration specific to a single project. Projects are configured in * `TestProject` encapsulates configuration specific to a single project. Projects are configured in
* [testConfig.projects](https://playwright.dev/docs/api/class-testconfig#test-config-projects) specified in the * [testConfig.projects](https://playwright.dev/docs/api/class-testconfig#test-config-projects) specified in the
@ -432,16 +434,22 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
* ``` * ```
* *
* The list of supported tokens: * The list of supported tokens:
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
* snapshot name.
* - Value: `foo/bar/baz`
* - `{ext}` - snapshot extension (with dots)
* - Value: `.png`
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{snapshotDir}` - Project's
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{testDir}` - Project's * - `{testDir}` - Project's
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir). * [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with * - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
* config) * config)
* - `{snapshotDir}` - Project's
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**. * - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* - Value: `page` * - Value: `page`
* - `{testFileName}` - Test file name with extension. * - `{testFileName}` - Test file name with extension.
@ -450,12 +458,6 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
* - Value: `page/page-click.spec.ts` * - Value: `page/page-click.spec.ts`
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name. * - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* - Value: `suite-test-should-work` * - Value: `suite-test-should-work`
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
* snapshot name.
* - Value: `foo/bar/baz`
* - `{ext}` - snapshot extension (with dots)
* - Value: `.png`
* *
* Each token can be preceded with a single character that will be used **only if** this token has non-empty value. * Each token can be preceded with a single character that will be used **only if** this token has non-empty value.
* *
@ -626,11 +628,13 @@ export interface Project<TestArgs = {}, WorkerArgs = {}> extends TestProject<Tes
} }
/** /**
* Runtime representation of the test project configuration that can be accessed in the tests via * Runtime representation of the test project configuration. It is accessible in the tests via
* [testInfo.project](https://playwright.dev/docs/api/class-testinfo#test-info-project) and * [testInfo.project](https://playwright.dev/docs/api/class-testinfo#test-info-project) and
* [workerInfo.project](https://playwright.dev/docs/api/class-workerinfo#worker-info-project). * [workerInfo.project](https://playwright.dev/docs/api/class-workerinfo#worker-info-project) and is passed to the
* test reporters. To see the format of the project in the Playwright configuration file please see {@link
* TestProject} instead.
*/ */
export interface ProjectInWorker<TestArgs = {}, WorkerArgs = {}> { export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
/** /**
* See [testProject.use](https://playwright.dev/docs/api/class-testproject#test-project-use). * See [testProject.use](https://playwright.dev/docs/api/class-testproject#test-project-use).
*/ */
@ -711,7 +715,8 @@ type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never });
/** /**
* Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` * Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout`
* or `testDir`. These options are described in the {@link TestConfig} object in the * or `testDir`. These options are described in the {@link TestConfig} object in the
* [configuration file](https://playwright.dev/docs/test-configuration). * [configuration file](https://playwright.dev/docs/test-configuration). This type describes format of the configuration file, to access
* resolved configuration parameters at run time use {@link FullConfig}.
* *
* Playwright Test supports running multiple test projects at the same time. Project-specific options should be put to * Playwright Test supports running multiple test projects at the same time. Project-specific options should be put to
* [testConfig.projects](https://playwright.dev/docs/api/class-testconfig#test-config-projects), but top-level {@link * [testConfig.projects](https://playwright.dev/docs/api/class-testconfig#test-config-projects), but top-level {@link
@ -929,24 +934,6 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* method. * method.
*/ */
toHaveScreenshot?: { toHaveScreenshot?: {
/**
* an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and
* `1` (lax). `"pixelmatch"` comparator computes color difference in
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
*/
threshold?: number;
/**
* an acceptable amount of pixels that could be different, unset by default.
*/
maxDiffPixels?: number;
/**
* an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by
* default.
*/
maxDiffPixelRatio?: number;
/** /**
* See `animations` in [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot). * See `animations` in [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot).
* Defaults to `"disabled"`. * Defaults to `"disabled"`.
@ -959,6 +946,17 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
*/ */
caret?: "hide"|"initial"; caret?: "hide"|"initial";
/**
* An acceptable amount of pixels that could be different, unset by default.
*/
maxDiffPixels?: number;
/**
* An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by
* default.
*/
maxDiffPixelRatio?: number;
/** /**
* See `scale` in [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot). Defaults * See `scale` in [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot). Defaults
* to `"css"`. * to `"css"`.
@ -969,6 +967,13 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* See `style` in [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot). * See `style` in [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot).
*/ */
stylePath?: string|Array<string>; stylePath?: string|Array<string>;
/**
* An acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and
* `1` (lax). `"pixelmatch"` comparator computes color difference in
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
*/
threshold?: number;
}; };
/** /**
@ -978,22 +983,22 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
*/ */
toMatchSnapshot?: { toMatchSnapshot?: {
/** /**
* an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and * An acceptable amount of pixels that could be different, unset by default.
* `1` (lax). `"pixelmatch"` comparator computes color difference in
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
*/
threshold?: number;
/**
* an acceptable amount of pixels that could be different, unset by default.
*/ */
maxDiffPixels?: number; maxDiffPixels?: number;
/** /**
* an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by * An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by
* default. * default.
*/ */
maxDiffPixelRatio?: number; maxDiffPixelRatio?: number;
/**
* An acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and
* `1` (lax). `"pixelmatch"` comparator computes color difference in
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
*/
threshold?: number;
}; };
/** /**
@ -1001,14 +1006,14 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
*/ */
toPass?: { toPass?: {
/** /**
* timeout for toPass method in milliseconds. * Probe intervals for toPass method in milliseconds.
*/
timeout?: number;
/**
* probe intervals for toPass method in milliseconds.
*/ */
intervals?: Array<number>; intervals?: Array<number>;
/**
* Timeout for toPass method in milliseconds.
*/
timeout?: number;
}; };
}; };
@ -1055,7 +1060,7 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
/** /**
* Path to the global setup file. This file will be required and run before all the tests. It must export a single * Path to the global setup file. This file will be required and run before all the tests. It must export a single
* function that takes a [`TestConfig`] argument. * function that takes a {@link FullConfig} argument.
* *
* Learn more about [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown). * Learn more about [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown).
* *
@ -1392,15 +1397,15 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* *
*/ */
shard?: null|{ shard?: null|{
/**
* The total number of shards.
*/
total: number;
/** /**
* The index of the shard to execute, one-based. * The index of the shard to execute, one-based.
*/ */
current: number; current: number;
/**
* The total number of shards.
*/
total: number;
}; };
/** /**
@ -1479,16 +1484,22 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* ``` * ```
* *
* The list of supported tokens: * The list of supported tokens:
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
* snapshot name.
* - Value: `foo/bar/baz`
* - `{ext}` - snapshot extension (with dots)
* - Value: `.png`
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{snapshotDir}` - Project's
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{testDir}` - Project's * - `{testDir}` - Project's
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir). * [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with * - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
* config) * config)
* - `{snapshotDir}` - Project's
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**. * - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* - Value: `page` * - Value: `page`
* - `{testFileName}` - Test file name with extension. * - `{testFileName}` - Test file name with extension.
@ -1497,12 +1508,6 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* - Value: `page/page-click.spec.ts` * - Value: `page/page-click.spec.ts`
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name. * - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* - Value: `suite-test-should-work` * - Value: `suite-test-should-work`
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
* snapshot name.
* - Value: `foo/bar/baz`
* - `{ext}` - snapshot extension (with dots)
* - Value: `.png`
* *
* Each token can be preceded with a single character that will be used **only if** this token has non-empty value. * Each token can be preceded with a single character that will be used **only if** this token has non-empty value.
* *
@ -1667,15 +1672,15 @@ export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig<TestA
export type Metadata = { [key: string]: any }; export type Metadata = { [key: string]: any };
/** /**
* Resolved configuration available via * Resolved configuration which is accessible via
* [testInfo.config](https://playwright.dev/docs/api/class-testinfo#test-info-config) and * [testInfo.config](https://playwright.dev/docs/api/class-testinfo#test-info-config) and is passed to the test
* [workerInfo.config](https://playwright.dev/docs/api/class-workerinfo#worker-info-config). * reporters. To see the format of Playwright configuration file, please see {@link TestConfig} instead.
*/ */
export interface ConfigInWorker<TestArgs = {}, WorkerArgs = {}> { export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
/** /**
* List of resolved projects. * List of resolved projects.
*/ */
projects: ProjectInWorker<TestArgs, WorkerArgs>[]; projects: FullProject<TestArgs, WorkerArgs>[];
/** /**
* See [testConfig.reporter](https://playwright.dev/docs/api/class-testconfig#test-config-reporter). * See [testConfig.reporter](https://playwright.dev/docs/api/class-testconfig#test-config-reporter).
*/ */
@ -1685,7 +1690,7 @@ export interface ConfigInWorker<TestArgs = {}, WorkerArgs = {}> {
*/ */
webServer: TestConfigWebServer | null; webServer: TestConfigWebServer | null;
/** /**
* Path to the configuration file (if any) used to run the tests. * Path to the configuration file used to run the tests. The value is an empty string if no config file was used.
*/ */
configFile?: string; configFile?: string;
@ -1759,6 +1764,9 @@ export interface ConfigInWorker<TestArgs = {}, WorkerArgs = {}> {
threshold: number; threshold: number;
}; };
/**
* Base directory for all relative paths used in the reporters.
*/
rootDir: string; rootDir: string;
/** /**
@ -8129,7 +8137,7 @@ export interface TestInfo {
/** /**
* Processed configuration from the [configuration file](https://playwright.dev/docs/test-configuration). * Processed configuration from the [configuration file](https://playwright.dev/docs/test-configuration).
*/ */
config: ConfigInWorker; config: FullConfig;
/** /**
* The number of milliseconds the test took to finish. Always zero before the test finishes, either successfully or * The number of milliseconds the test took to finish. Always zero before the test finishes, either successfully or
@ -8205,7 +8213,7 @@ export interface TestInfo {
/** /**
* Processed project configuration from the [configuration file](https://playwright.dev/docs/test-configuration). * Processed project configuration from the [configuration file](https://playwright.dev/docs/test-configuration).
*/ */
project: ProjectInWorker; project: FullProject;
/** /**
* Specifies a unique repeat index when running in "repeat each" mode. This mode is enabled by passing `--repeat-each` * Specifies a unique repeat index when running in "repeat each" mode. This mode is enabled by passing `--repeat-each`
@ -8359,7 +8367,7 @@ export interface WorkerInfo {
/** /**
* Processed configuration from the [configuration file](https://playwright.dev/docs/test-configuration). * Processed configuration from the [configuration file](https://playwright.dev/docs/test-configuration).
*/ */
config: ConfigInWorker; config: FullConfig;
/** /**
* The index of the worker between `0` and `workers - 1`. It is guaranteed that workers running at the same time have * The index of the worker between `0` and `workers - 1`. It is guaranteed that workers running at the same time have
@ -8374,7 +8382,7 @@ export interface WorkerInfo {
/** /**
* Processed project configuration from the [configuration file](https://playwright.dev/docs/test-configuration). * Processed project configuration from the [configuration file](https://playwright.dev/docs/test-configuration).
*/ */
project: ProjectInWorker; project: FullProject;
/** /**
* The unique index of the worker process that is running the test. When a worker is restarted, for example after a * The unique index of the worker process that is running the test. When a worker is restarted, for example after a
@ -8393,17 +8401,14 @@ interface TestConfigWebServer {
command: string; command: string;
/** /**
* The port that your http server is expected to appear on. It does wait until it accepts connections. Either `port` * Current working directory of the spawned process, defaults to the directory of the configuration file.
* or `url` should be specified.
*/ */
port?: number; cwd?: string;
/** /**
* The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the * Environment variables to set for the command, `process.env` by default.
* server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is
* checked. Either `port` or `url` should be specified.
*/ */
url?: string; env?: { [key: string]: string; };
/** /**
* Whether to ignore HTTPS errors when fetching the `url`. Defaults to `false`. * Whether to ignore HTTPS errors when fetching the `url`. Defaults to `false`.
@ -8411,9 +8416,10 @@ interface TestConfigWebServer {
ignoreHTTPSErrors?: boolean; ignoreHTTPSErrors?: boolean;
/** /**
* How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. * The port that your http server is expected to appear on. It does wait until it accepts connections. Either `port`
* or `url` should be specified.
*/ */
timeout?: number; port?: number;
/** /**
* If true, it will re-use an existing server on the `port` or `url` when available. If no server is running on that * If true, it will re-use an existing server on the `port` or `url` when available. If no server is running on that
@ -8435,13 +8441,15 @@ interface TestConfigWebServer {
stderr?: "pipe"|"ignore"; stderr?: "pipe"|"ignore";
/** /**
* Current working directory of the spawned process, defaults to the directory of the configuration file. * How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
*/ */
cwd?: string; timeout?: number;
/** /**
* Environment variables to set for the command, `process.env` by default. * 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.
*/ */
env?: { [key: string]: string; }; url?: string;
} }

View file

@ -15,217 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import type { TestStatus, Metadata, PlaywrightTestOptions, PlaywrightWorkerOptions, ReporterDescription, ConfigInWorker } from './test'; import type { TestStatus, Metadata, PlaywrightTestOptions, PlaywrightWorkerOptions, ReporterDescription, FullConfig, FullProject } from './test';
export type { TestStatus } from './test'; export type { FullConfig, FullProject, TestStatus } from './test';
type UseOptions<TestArgs, WorkerArgs> = Partial<WorkerArgs> & Partial<TestArgs>;
/**
* Resolved configuration passed to
* [reporter.onBegin(config, suite)](https://playwright.dev/docs/api/class-reporter#reporter-on-begin).
*/
export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* List of resolved projects.
*/
projects: FullProject<TestArgs, WorkerArgs>[];
/**
* See [testConfig.reporter](https://playwright.dev/docs/api/class-testconfig#test-config-reporter).
*/
reporter: ReporterDescription[];
/**
* See [testConfig.webServer](https://playwright.dev/docs/api/class-testconfig#test-config-web-server).
*/
webServer: ConfigInWorker['webServer'];
/**
* Path to the configuration file (if any) used to run the tests.
*/
configFile?: string;
/**
* See [testConfig.forbidOnly](https://playwright.dev/docs/api/class-testconfig#test-config-forbid-only).
*/
forbidOnly: boolean;
/**
* See [testConfig.fullyParallel](https://playwright.dev/docs/api/class-testconfig#test-config-fully-parallel).
*/
fullyParallel: boolean;
/**
* See [testConfig.globalSetup](https://playwright.dev/docs/api/class-testconfig#test-config-global-setup).
*/
globalSetup: null|string;
/**
* See [testConfig.globalTeardown](https://playwright.dev/docs/api/class-testconfig#test-config-global-teardown).
*/
globalTeardown: null|string;
/**
* See [testConfig.globalTimeout](https://playwright.dev/docs/api/class-testconfig#test-config-global-timeout).
*/
globalTimeout: number;
/**
* See [testConfig.grep](https://playwright.dev/docs/api/class-testconfig#test-config-grep).
*/
grep: RegExp|Array<RegExp>;
/**
* See [testConfig.grepInvert](https://playwright.dev/docs/api/class-testconfig#test-config-grep-invert).
*/
grepInvert: null|RegExp|Array<RegExp>;
/**
* See [testConfig.maxFailures](https://playwright.dev/docs/api/class-testconfig#test-config-max-failures).
*/
maxFailures: number;
/**
* See [testConfig.metadata](https://playwright.dev/docs/api/class-testconfig#test-config-metadata).
*/
metadata: Metadata;
/**
* See [testConfig.preserveOutput](https://playwright.dev/docs/api/class-testconfig#test-config-preserve-output).
*/
preserveOutput: "always"|"never"|"failures-only";
/**
* See [testConfig.quiet](https://playwright.dev/docs/api/class-testconfig#test-config-quiet).
*/
quiet: boolean;
/**
* See [testConfig.reportSlowTests](https://playwright.dev/docs/api/class-testconfig#test-config-report-slow-tests).
*/
reportSlowTests: null|{
/**
* The maximum number of slow test files to report. Defaults to `5`.
*/
max: number;
/**
* Test duration in milliseconds that is considered slow. Defaults to 15 seconds.
*/
threshold: number;
};
rootDir: string;
/**
* See [testConfig.shard](https://playwright.dev/docs/api/class-testconfig#test-config-shard).
*/
shard: null|{
/**
* The total number of shards.
*/
total: number;
/**
* The index of the shard to execute, one-based.
*/
current: number;
};
/**
* See [testConfig.updateSnapshots](https://playwright.dev/docs/api/class-testconfig#test-config-update-snapshots).
*/
updateSnapshots: "all"|"none"|"missing";
/**
* Playwright version.
*/
version: string;
/**
* See [testConfig.workers](https://playwright.dev/docs/api/class-testconfig#test-config-workers).
*/
workers: number;
}
/**
* Runtime representation of the test project configuration that is passed to {@link Reporter}. It exposes some of the
* resolved fields declared in {@link TestProject}. You can get {@link FullProject} instance from
* [fullConfig.projects](https://playwright.dev/docs/api/class-fullconfig#full-config-projects) or
* [suite.project()](https://playwright.dev/docs/api/class-suite#suite-project).
*/
export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
/**
* See [testProject.use](https://playwright.dev/docs/api/class-testproject#test-project-use).
*/
use: UseOptions<PlaywrightTestOptions & TestArgs, PlaywrightWorkerOptions & WorkerArgs>;
/**
* See [testProject.dependencies](https://playwright.dev/docs/api/class-testproject#test-project-dependencies).
*/
dependencies: Array<string>;
/**
* See [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep).
*/
grep: RegExp|Array<RegExp>;
/**
* See [testProject.grepInvert](https://playwright.dev/docs/api/class-testproject#test-project-grep-invert).
*/
grepInvert: null|RegExp|Array<RegExp>;
/**
* See [testProject.metadata](https://playwright.dev/docs/api/class-testproject#test-project-metadata).
*/
metadata: Metadata;
/**
* See [testProject.name](https://playwright.dev/docs/api/class-testproject#test-project-name).
*/
name: string;
/**
* See [testProject.outputDir](https://playwright.dev/docs/api/class-testproject#test-project-output-dir).
*/
outputDir: string;
/**
* See [testProject.repeatEach](https://playwright.dev/docs/api/class-testproject#test-project-repeat-each).
*/
repeatEach: number;
/**
* See [testProject.retries](https://playwright.dev/docs/api/class-testproject#test-project-retries).
*/
retries: number;
/**
* See [testProject.snapshotDir](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-dir).
*/
snapshotDir: string;
/**
* See [testProject.teardown](https://playwright.dev/docs/api/class-testproject#test-project-teardown).
*/
teardown?: string;
/**
* See [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
*/
testDir: string;
/**
* See [testProject.testIgnore](https://playwright.dev/docs/api/class-testproject#test-project-test-ignore).
*/
testIgnore: string|RegExp|Array<string|RegExp>;
/**
* See [testProject.testMatch](https://playwright.dev/docs/api/class-testproject#test-project-test-match).
*/
testMatch: string|RegExp|Array<string|RegExp>;
/**
* See [testProject.timeout](https://playwright.dev/docs/api/class-testproject#test-project-timeout).
*/
timeout: number;
}
/** /**
* Result of the full test run. * Result of the full test run.
@ -578,7 +369,7 @@ export interface Suite {
/** /**
* Test cases and suites defined directly in this suite. The elements are returned in their declaration order. You can * Test cases and suites defined directly in this suite. The elements are returned in their declaration order. You can
* discriminate between different entry types using * differentiate between various entry types by using
* [testCase.type](https://playwright.dev/docs/api/class-testcase#test-case-type) and * [testCase.type](https://playwright.dev/docs/api/class-testcase#test-case-type) and
* [suite.type](https://playwright.dev/docs/api/class-suite#suite-type). * [suite.type](https://playwright.dev/docs/api/class-suite#suite-type).
*/ */
@ -770,7 +561,8 @@ export interface TestCase {
title: string; title: string;
/** /**
* Returns type of the test. * Returns "test". Useful for detecting test cases in
* [suite.entries()](https://playwright.dev/docs/api/class-suite#suite-entries).
*/ */
type: "test"; type: "test";
} }

View file

@ -1790,6 +1790,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
goForward(params: PageGoForwardParams, metadata?: CallMetadata): Promise<PageGoForwardResult>; goForward(params: PageGoForwardParams, metadata?: CallMetadata): Promise<PageGoForwardResult>;
registerLocatorHandler(params: PageRegisterLocatorHandlerParams, metadata?: CallMetadata): Promise<PageRegisterLocatorHandlerResult>; registerLocatorHandler(params: PageRegisterLocatorHandlerParams, metadata?: CallMetadata): Promise<PageRegisterLocatorHandlerResult>;
resolveLocatorHandlerNoReply(params: PageResolveLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise<PageResolveLocatorHandlerNoReplyResult>; resolveLocatorHandlerNoReply(params: PageResolveLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise<PageResolveLocatorHandlerNoReplyResult>;
unregisterLocatorHandlerNoReply(params: PageUnregisterLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise<PageUnregisterLocatorHandlerNoReplyResult>;
reload(params: PageReloadParams, metadata?: CallMetadata): Promise<PageReloadResult>; reload(params: PageReloadParams, metadata?: CallMetadata): Promise<PageReloadResult>;
expectScreenshot(params: PageExpectScreenshotParams, metadata?: CallMetadata): Promise<PageExpectScreenshotResult>; expectScreenshot(params: PageExpectScreenshotParams, metadata?: CallMetadata): Promise<PageExpectScreenshotResult>;
screenshot(params: PageScreenshotParams, metadata?: CallMetadata): Promise<PageScreenshotResult>; screenshot(params: PageScreenshotParams, metadata?: CallMetadata): Promise<PageScreenshotResult>;
@ -1926,20 +1927,29 @@ export type PageGoForwardResult = {
}; };
export type PageRegisterLocatorHandlerParams = { export type PageRegisterLocatorHandlerParams = {
selector: string, selector: string,
allowStayingVisible?: boolean,
}; };
export type PageRegisterLocatorHandlerOptions = { export type PageRegisterLocatorHandlerOptions = {
allowStayingVisible?: boolean,
}; };
export type PageRegisterLocatorHandlerResult = { export type PageRegisterLocatorHandlerResult = {
uid: number, uid: number,
}; };
export type PageResolveLocatorHandlerNoReplyParams = { export type PageResolveLocatorHandlerNoReplyParams = {
uid: number, uid: number,
remove?: boolean,
}; };
export type PageResolveLocatorHandlerNoReplyOptions = { export type PageResolveLocatorHandlerNoReplyOptions = {
remove?: boolean,
}; };
export type PageResolveLocatorHandlerNoReplyResult = void; export type PageResolveLocatorHandlerNoReplyResult = void;
export type PageUnregisterLocatorHandlerNoReplyParams = {
uid: number,
};
export type PageUnregisterLocatorHandlerNoReplyOptions = {
};
export type PageUnregisterLocatorHandlerNoReplyResult = void;
export type PageReloadParams = { export type PageReloadParams = {
timeout?: number, timeout?: number,
waitUntil?: LifecycleEvent, waitUntil?: LifecycleEvent,

View file

@ -1350,10 +1350,16 @@ Page:
registerLocatorHandler: registerLocatorHandler:
parameters: parameters:
selector: string selector: string
allowStayingVisible: boolean?
returns: returns:
uid: number uid: number
resolveLocatorHandlerNoReply: resolveLocatorHandlerNoReply:
parameters:
uid: number
remove: boolean?
unregisterLocatorHandlerNoReply:
parameters: parameters:
uid: number uid: number

View file

@ -115,11 +115,7 @@ export class TeleSuiteUpdater {
this._options.onUpdate(); this._options.onUpdate();
}, },
onError: (error: reporterTypes.TestError) => { onError: (error: reporterTypes.TestError) => this._handleOnError(error),
this.loadErrors.push(error);
this._options.onError?.(error);
this._options.onUpdate();
},
printsToStdio: () => { printsToStdio: () => {
return false; return false;
@ -133,6 +129,17 @@ export class TeleSuiteUpdater {
}; };
} }
processGlobalReport(report: any[]) {
const receiver = new TeleReporterReceiver({
onConfigure: (c: reporterTypes.FullConfig) => {
this.config = c;
},
onError: (error: reporterTypes.TestError) => this._handleOnError(error)
});
for (const message of report)
receiver.dispatch(message);
}
processListReport(report: any[]) { processListReport(report: any[]) {
// Save test results and reset all projects, the results will be restored after // Save test results and reset all projects, the results will be restored after
// new project structure is built. // new project structure is built.
@ -150,6 +157,12 @@ export class TeleSuiteUpdater {
this._receiver.dispatch(message)?.catch(() => {}); this._receiver.dispatch(message)?.catch(() => {});
} }
private _handleOnError(error: reporterTypes.TestError) {
this.loadErrors.push(error);
this._options.onError?.(error);
this._options.onUpdate();
}
asModel(): TestModel { asModel(): TestModel {
return { return {
rootSuite: this.rootSuite || new TeleSuite('', 'root'), rootSuite: this.rootSuite || new TeleSuite('', 'root'),

View file

@ -172,24 +172,29 @@ export const UIModeView: React.FC<{}> = ({
setIsLoading(true); setIsLoading(true);
setWatchedTreeIds({ value: new Set() }); setWatchedTreeIds({ value: new Set() });
(async () => { (async () => {
await testServerConnection.initialize({ try {
interceptStdio: true, await testServerConnection.initialize({
watchTestDirs: true interceptStdio: true,
}); watchTestDirs: true
const { status } = await testServerConnection.runGlobalSetup({}); });
if (status !== 'passed') const { status, report } = await testServerConnection.runGlobalSetup({});
return; teleSuiteUpdater.processGlobalReport(report);
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args }); if (status !== 'passed')
teleSuiteUpdater.processListReport(result.report); return;
testServerConnection.onListChanged(updateList); const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });
testServerConnection.onReport(params => { teleSuiteUpdater.processListReport(result.report);
teleSuiteUpdater.processTestReportEvent(params);
});
setIsLoading(false);
const { hasBrowsers } = await testServerConnection.checkBrowsers({}); testServerConnection.onListChanged(updateList);
setHasBrowsers(hasBrowsers); testServerConnection.onReport(params => {
teleSuiteUpdater.processTestReportEvent(params);
});
const { hasBrowsers } = await testServerConnection.checkBrowsers({});
setHasBrowsers(hasBrowsers);
} finally {
setIsLoading(false);
}
})(); })();
return () => { return () => {
clearTimeout(throttleTimer); clearTimeout(throttleTimer);

View file

@ -50,9 +50,16 @@
}, false); }, false);
close.addEventListener('click', () => { close.addEventListener('click', () => {
interstitial.classList.remove('visible'); const closeInterstitial = () => {
target.classList.remove('hidden'); interstitial.classList.remove('visible');
target.classList.remove('removed'); target.classList.remove('hidden');
target.classList.remove('removed');
};
if (interstitial.classList.contains('timeout'))
setTimeout(closeInterstitial, 3000);
else
closeInterstitial();
}); });
let timesToShow = 0; let timesToShow = 0;
@ -65,9 +72,11 @@
if (!timesToShow && event !== 'none') if (!timesToShow && event !== 'none')
target.removeEventListener(event, listener, capture === 'capture'); target.removeEventListener(event, listener, capture === 'capture');
}; };
if (event === 'hide') { if (event === 'hide' || event === 'timeout') {
target.classList.add('hidden'); target.classList.add('hidden');
listener(); listener();
if (event === 'timeout')
interstitial.classList.add('timeout');
} else if (event === 'remove') { } else if (event === 'remove') {
target.classList.add('removed'); target.classList.add('removed');
listener(); listener();

View file

@ -22,7 +22,9 @@ test('should work', async ({ page, server }) => {
let beforeCount = 0; let beforeCount = 0;
let afterCount = 0; let afterCount = 0;
await page.addLocatorHandler(page.getByText('This interstitial covers the button'), async () => { const originalLocator = page.getByText('This interstitial covers the button');
await page.addLocatorHandler(originalLocator, async locatorArgument => {
expect(locatorArgument).toBe(originalLocator);
++beforeCount; ++beforeCount;
await page.locator('#close').click(); await page.locator('#close').click();
++afterCount; ++afterCount;
@ -64,7 +66,7 @@ test('should work with a custom check', async ({ page, server }) => {
await page.addLocatorHandler(page.locator('body'), async () => { await page.addLocatorHandler(page.locator('body'), async () => {
if (await page.getByText('This interstitial covers the button').isVisible()) if (await page.getByText('This interstitial covers the button').isVisible())
await page.locator('#close').click(); await page.locator('#close').click();
}); }, { allowStayingVisible: true });
for (const args of [ for (const args of [
['mouseover', 2], ['mouseover', 2],
@ -204,3 +206,129 @@ test('should work with toHaveScreenshot', async ({ page, server, isAndroid }) =>
await expect(page).toHaveScreenshot('screenshot-grid.png'); await expect(page).toHaveScreenshot('screenshot-grid.png');
}); });
test('should work when owner frame detaches', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
const iframe = document.createElement('iframe');
iframe.src = 'data:text/html,<body>hello from iframe</body>';
document.body.append(iframe);
const target = document.createElement('button');
target.textContent = 'Click me';
target.id = 'target';
target.addEventListener('click', () => (window as any)._clicked = true);
document.body.appendChild(target);
const closeButton = document.createElement('button');
closeButton.textContent = 'close';
closeButton.id = 'close';
closeButton.addEventListener('click', () => iframe.remove());
document.body.appendChild(closeButton);
});
await page.addLocatorHandler(page.frameLocator('iframe').locator('body'), async () => {
await page.locator('#close').click();
});
await page.locator('#target').click();
expect(await page.$('iframe')).toBe(null);
expect(await page.evaluate('window._clicked')).toBe(true);
});
test('should work with times: option', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/handle-locator.html');
let called = 0;
await page.addLocatorHandler(page.locator('body'), async () => {
++called;
}, { allowStayingVisible: true, times: 2 });
await page.locator('#aside').hover();
await page.evaluate(() => {
(window as any).clicked = 0;
(window as any).setupAnnoyingInterstitial('mouseover', 4);
});
const error = await page.locator('#target').click({ timeout: 3000 }).catch(e => e);
expect(called).toBe(2);
expect(await page.evaluate('window.clicked')).toBe(0);
await expect(page.locator('#interstitial')).toBeVisible();
expect(error.message).toContain('Timeout 3000ms exceeded');
expect(error.message).toContain(`<div>This interstitial covers the button</div> from <div class="visible" id="interstitial">…</div> subtree intercepts pointer events`);
});
test('should wait for hidden by default', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/handle-locator.html');
let called = 0;
await page.addLocatorHandler(page.getByRole('button', { name: 'close' }), async button => {
called++;
await button.click();
});
await page.locator('#aside').hover();
await page.evaluate(() => {
(window as any).clicked = 0;
(window as any).setupAnnoyingInterstitial('timeout', 1);
});
await page.locator('#target').click();
expect(await page.evaluate('window.clicked')).toBe(1);
await expect(page.locator('#interstitial')).not.toBeVisible();
expect(called).toBe(1);
});
test('should work with allowStayingVisible', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/handle-locator.html');
let called = 0;
await page.addLocatorHandler(page.getByRole('button', { name: 'close' }), async button => {
called++;
if (called === 1)
await button.click();
else
await page.locator('#interstitial').waitFor({ state: 'hidden' });
}, { allowStayingVisible: true });
await page.locator('#aside').hover();
await page.evaluate(() => {
(window as any).clicked = 0;
(window as any).setupAnnoyingInterstitial('timeout', 1);
});
await page.locator('#target').click();
expect(await page.evaluate('window.clicked')).toBe(1);
await expect(page.locator('#interstitial')).not.toBeVisible();
expect(called).toBe(2);
});
test('should removeLocatorHandler', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/handle-locator.html');
let called = 0;
const handler = async locator => {
++called;
await locator.click();
};
await page.addLocatorHandler(page.getByRole('button', { name: 'close' }), handler);
await page.evaluate(() => {
(window as any).clicked = 0;
(window as any).setupAnnoyingInterstitial('hide', 1);
});
await page.locator('#target').click();
expect(called).toBe(1);
expect(await page.evaluate('window.clicked')).toBe(1);
await expect(page.locator('#interstitial')).not.toBeVisible();
await page.evaluate(() => {
(window as any).clicked = 0;
(window as any).setupAnnoyingInterstitial('hide', 1);
});
await page.removeLocatorHandler(page.getByRole('button', { name: 'close' }), handler);
const error = await page.locator('#target').click({ timeout: 3000 }).catch(e => e);
expect(called).toBe(1);
expect(await page.evaluate('window.clicked')).toBe(0);
await expect(page.locator('#interstitial')).toBeVisible();
expect(error.message).toContain('Timeout 3000ms exceeded');
});

View file

@ -43,9 +43,7 @@ it('should allow accepting prompts @smoke', async ({ page, isElectron }) => {
it('should dismiss the prompt', async ({ page, isElectron }) => { it('should dismiss the prompt', async ({ page, isElectron }) => {
it.skip(isElectron, 'prompt() is not a thing in electron'); it.skip(isElectron, 'prompt() is not a thing in electron');
page.on('dialog', dialog => { page.on('dialog', dialog => dialog.dismiss());
void dialog.dismiss();
});
const result = await page.evaluate(() => prompt('question?')); const result = await page.evaluate(() => prompt('question?'));
expect(result).toBe(null); expect(result).toBe(null);
}); });

View file

@ -19,6 +19,14 @@ import { test as it, expect } from './pageTest';
import { attachFrame } from '../config/utils'; import { attachFrame } from '../config/utils';
import fs from 'fs'; import fs from 'fs';
function adjustServerHeaders(headers: Object, browserName: string, channel: string) {
if (browserName === 'firefox' && channel === 'firefox-beta') {
// This is a new experimental feature, only enabled in Firefox Beta for now.
delete headers['priority'];
}
return headers;
}
it('should work for main frame navigation request', async ({ page, server }) => { it('should work for main frame navigation request', async ({ page, server }) => {
const requests = []; const requests = [];
page.on('request', request => requests.push(request)); page.on('request', request => requests.push(request));
@ -82,7 +90,7 @@ it('should return headers', async ({ page, server, browserName }) => {
expect(response.request().headers()['user-agent']).toContain('WebKit'); expect(response.request().headers()['user-agent']).toContain('WebKit');
}); });
it('should get the same headers as the server', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => { it('should get the same headers as the server', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99'); it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language'); it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language');
let serverRequest; let serverRequest;
@ -92,10 +100,10 @@ it('should get the same headers as the server', async ({ page, server, browserNa
}); });
const response = await page.goto(server.PREFIX + '/empty.html'); const response = await page.goto(server.PREFIX + '/empty.html');
const headers = await response.request().allHeaders(); const headers = await response.request().allHeaders();
expect(headers).toEqual(serverRequest.headers); expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName, channel));
}); });
it('should not return allHeaders() until they are available', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => { it('should not return allHeaders() until they are available', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99'); it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language'); it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language');
@ -114,13 +122,13 @@ it('should not return allHeaders() until they are available', async ({ page, ser
await page.goto(server.PREFIX + '/empty.html'); await page.goto(server.PREFIX + '/empty.html');
const requestHeaders = await requestHeadersPromise; const requestHeaders = await requestHeadersPromise;
expect(requestHeaders).toEqual(serverRequest.headers); expect(requestHeaders).toEqual(adjustServerHeaders(serverRequest.headers, browserName, channel));
const responseHeaders = await responseHeadersPromise; const responseHeaders = await responseHeadersPromise;
expect(responseHeaders['foo']).toBe('bar'); expect(responseHeaders['foo']).toBe('bar');
}); });
it('should get the same headers as the server CORS', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => { it('should get the same headers as the server CORS', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99'); it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language'); it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language');
@ -139,7 +147,7 @@ it('should get the same headers as the server CORS', async ({ page, server, brow
expect(text).toBe('done'); expect(text).toBe('done');
const response = await responsePromise; const response = await responsePromise;
const headers = await response.request().allHeaders(); const headers = await response.request().allHeaders();
expect(headers).toEqual(serverRequest.headers); expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName, channel));
}); });
it('should not get preflight CORS requests when intercepting', async ({ page, server, browserName, isAndroid }) => { it('should not get preflight CORS requests when intercepting', async ({ page, server, browserName, isAndroid }) => {
@ -353,7 +361,7 @@ it('should return navigation bit when navigating to image', async ({ page, serve
expect(requests[0].isNavigationRequest()).toBe(true); expect(requests[0].isNavigationRequest()).toBe(true);
}); });
it('should report raw headers', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => { it('should report raw headers', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99'); it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
let expectedHeaders: { name: string, value: string }[]; let expectedHeaders: { name: string, value: string }[];
@ -376,6 +384,10 @@ it('should report raw headers', async ({ page, server, browserName, platform, is
return { name, value: values[0] }; return { name, value: values[0] };
}); });
} }
if (browserName === 'firefox' && channel === 'firefox-beta') {
// This is a new experimental feature, only enabled in Firefox Beta for now.
expectedHeaders = expectedHeaders.filter(({ name }) => name.toLowerCase() !== 'priority');
}
res.end(); res.end();
}); });
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);

View file

@ -280,8 +280,8 @@ const authFiles = {
export default config; export default config;
`, `,
'auth.ts': ` 'auth.ts': `
import { chromium, ConfigInWorker } from '@playwright/test'; import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: ConfigInWorker) { async function globalSetup(config: FullConfig) {
const { baseURL, storageState } = config.projects[0].use; const { baseURL, storageState } = config.projects[0].use;
const browser = await chromium.launch(); const browser = await chromium.launch();
const page = await browser.newPage(); const page = await browser.newPage();

View file

@ -665,3 +665,33 @@ test('should pass undefined value as param', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
}); });
test('should resolve components imported from node_modules', async ({ runInlineTest }) => {
const result = await runInlineTest({
'package.json': `{ "name": "test-project" }`,
'playwright.config.ts': playwrightCtConfigText,
'playwright/index.html': `<script type="module" src="./index.js"></script>`,
'playwright/index.js': ``,
'node_modules/@mui/material/index.js': `
const TextField = () => 'input';
module.exports = { TextField };
`,
'node_modules/@mui/material/package.json': JSON.stringify({
name: '@mui/material',
main: './index.js',
}),
'src/component.spec.tsx': `
import { test } from '@playwright/experimental-ct-react';
import { TextField } from '@mui/material';
test("passes", async ({ mount }) => {
await mount(<TextField />);
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

View file

@ -141,12 +141,8 @@ for (const useIntermediateMergeReport of [false, true] as const) {
'a.test.ts': ` 'a.test.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('not run', async ({}) => { test('not run', async ({}) => {
console.log('log');
console.error('error');
}); });
test.only('is run', async ({}) => { test.only('is run', async ({}) => {
console.log('log');
console.error('error');
}); });
` `
}, { reporter: '', workers: 1 }); }, { reporter: '', workers: 1 });
@ -156,16 +152,10 @@ for (const useIntermediateMergeReport of [false, true] as const) {
onBegin: 3 tests total onBegin: 3 tests total
options.onBegin=begin-data options.onBegin=begin-data
onTestBegin: foo > a.test.ts > is run; retry #0 onTestBegin: foo > a.test.ts > is run; retry #0
log
error
onTestEnd: foo > a.test.ts > is run; retry #0 onTestEnd: foo > a.test.ts > is run; retry #0
onTestBegin: foo > a.test.ts > is run; retry #0 onTestBegin: foo > a.test.ts > is run; retry #0
log
error
onTestEnd: foo > a.test.ts > is run; retry #0 onTestEnd: foo > a.test.ts > is run; retry #0
onTestBegin: bar > a.test.ts > is run; retry #0 onTestBegin: bar > a.test.ts > is run; retry #0
log
error
onTestEnd: bar > a.test.ts > is run; retry #0 onTestEnd: bar > a.test.ts > is run; retry #0
onEnd onEnd
options.onEnd=end-data options.onEnd=end-data

View file

@ -84,6 +84,17 @@ test('should teardown on sigint', async ({ runUITest, nodeVersion }) => {
]); ]);
}); });
test('should show errors in config', async ({ runUITest }) => {
const { page } = await runUITest({
'playwright.config.ts': `
import { defineConfig, devices } from '@playwright/test';
throw new Error("URL is empty")
`,
});
await page.getByText('playwright.config.ts').click();
await expect(page.getByText('Error: URL is empty')).toBeInViewport();
});
const testsWithSetup = { const testsWithSetup = {
'playwright.config.ts': ` 'playwright.config.ts': `
import { defineConfig } from '@playwright/test'; import { defineConfig } from '@playwright/test';

View file

@ -14,11 +14,11 @@ Azure Functions Core Tools is not available on macOS M1 yet, so we use GitHub Co
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
mv microsoft.gpg /etc/apt/trusted.gpg.d/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' 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 apt-get update && apt-get install azure-functions-core-tools-4 sudo
``` ```
- Install Azure CLI: - Install Azure CLI:
```bash ```bash
curl -sL https://aka.ms/InstallAzureCLIDeb | bash curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
``` ```
- Login to Azure: - Login to Azure:
```bash ```bash

View file

@ -16,6 +16,6 @@
}, },
"extensionBundle": { "extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle", "id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[2.*, 3.0.0)" "version": "[4.*, 5.0.0)"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,10 @@
"name": "flakiness-dashboard", "name": "flakiness-dashboard",
"version": "", "version": "",
"description": "", "description": "",
"scripts": { "main": "processing/index.js",
"test": "echo \"No tests yet...\""
},
"author": "", "author": "",
"dependencies": { "dependencies": {
"@azure/storage-blob": "^12.16.0" "@azure/identity": "^4.1.0",
"@azure/storage-blob": "^12.17.0"
} }
} }

View file

@ -1,13 +0,0 @@
{
"version": "2.0",
"extensions": {
"queues": {
"batchSize": 1,
"newBatchThreshold": 0
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[1.*, 2.0.0)"
}
}

View file

@ -13,14 +13,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const { BlobServiceClient } = require("@azure/storage-blob"); // @ts-check
const { DefaultAzureCredential } = require('@azure/identity');
const { BlobServiceClient } = require('@azure/storage-blob');
const defaultAzureCredential = new DefaultAzureCredential();
const zlib = require('zlib'); const zlib = require('zlib');
const util = require('util'); const util = require('util');
const gzipAsync = util.promisify(zlib.gzip); const gzipAsync = util.promisify(zlib.gzip);
const gunzipAsync = util.promisify(zlib.gunzip); const gunzipAsync = util.promisify(zlib.gunzip);
const blobServiceClient = BlobServiceClient.fromConnectionString(process.env.AzureWebJobsStorage); const AZURE_STORAGE_ACCOUNT = 'folioflakinessdashboard';
const blobServiceClient = new BlobServiceClient(
`https://${AZURE_STORAGE_ACCOUNT}.blob.core.windows.net`,
defaultAzureCredential
);
function flattenSpecs(suite, result = [], titlePaths = []) { function flattenSpecs(suite, result = [], titlePaths = []) {
if (suite.suites) { if (suite.suites) {

View file

@ -283,7 +283,7 @@ class TypesGenerator {
parts.push(this.writeComment(comment, indent)); parts.push(this.writeComment(comment, indent));
else else
parts.push(this.writeComment(commentForMethod[method], indent)); parts.push(this.writeComment(commentForMethod[method], indent));
parts.push(` ${method}(event: '${eventName}', listener: (${params}) => void): this;\n`); parts.push(` ${method}(event: '${eventName}', listener: (${params}) => any): this;\n`);
} }
} }

View file

@ -39,7 +39,7 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
export interface Project<TestArgs = {}, WorkerArgs = {}> extends TestProject<TestArgs, WorkerArgs> { export interface Project<TestArgs = {}, WorkerArgs = {}> extends TestProject<TestArgs, WorkerArgs> {
} }
export interface ProjectInWorker<TestArgs = {}, WorkerArgs = {}> { export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
use: UseOptions<PlaywrightTestOptions & TestArgs, PlaywrightWorkerOptions & WorkerArgs>; use: UseOptions<PlaywrightTestOptions & TestArgs, PlaywrightWorkerOptions & WorkerArgs>;
} }
@ -57,8 +57,8 @@ export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig<TestA
export type Metadata = { [key: string]: any }; export type Metadata = { [key: string]: any };
export interface ConfigInWorker<TestArgs = {}, WorkerArgs = {}> { export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
projects: ProjectInWorker<TestArgs, WorkerArgs>[]; projects: FullProject<TestArgs, WorkerArgs>[];
reporter: ReporterDescription[]; reporter: ReporterDescription[];
webServer: TestConfigWebServer | null; webServer: TestConfigWebServer | null;
} }

View file

@ -14,20 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import type { TestStatus, Metadata, PlaywrightTestOptions, PlaywrightWorkerOptions, ReporterDescription, ConfigInWorker } from './test'; import type { TestStatus, Metadata, PlaywrightTestOptions, PlaywrightWorkerOptions, ReporterDescription, FullConfig, FullProject } from './test';
export type { TestStatus } from './test'; export type { FullConfig, FullProject, TestStatus } from './test';
type UseOptions<TestArgs, WorkerArgs> = Partial<WorkerArgs> & Partial<TestArgs>;
export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
projects: FullProject<TestArgs, WorkerArgs>[];
reporter: ReporterDescription[];
webServer: ConfigInWorker['webServer'];
}
export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
use: UseOptions<PlaywrightTestOptions & TestArgs, PlaywrightWorkerOptions & WorkerArgs>;
}
/** /**
* Result of the full test run. * Result of the full test run.

View file

@ -154,8 +154,6 @@ playwright.chromium.launch().then(async browser => {
return 'something random for no reason'; return 'something random for no reason';
}); });
await page.addLocatorHandler(page.locator(''), () => {});
await page.addLocatorHandler(page.locator(''), () => 42);
await page.addLocatorHandler(page.locator(''), async () => { }); await page.addLocatorHandler(page.locator(''), async () => { });
await page.addLocatorHandler(page.locator(''), async () => 42); await page.addLocatorHandler(page.locator(''), async () => 42);
await page.addLocatorHandler(page.locator(''), () => Promise.resolve(42)); await page.addLocatorHandler(page.locator(''), () => Promise.resolve(42));
@ -921,6 +919,10 @@ playwright.chromium.launch().then(async browser => {
.removeListener('close', listener) .removeListener('close', listener)
.off('close', listener); .off('close', listener);
} }
{
const page: playwright.Page = {} as any;
page.on('dialog', dialog => dialog.accept());
}
}); });
// waitForResponse callback predicate // waitForResponse callback predicate