Merge branch 'main' into main
Signed-off-by: Alex Schwartz <alexschwartz01@gmail.com>
This commit is contained in:
commit
123bc22dbb
|
|
@ -1,6 +1,6 @@
|
||||||
# 🎭 Playwright
|
# 🎭 Playwright
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||||
|
|
||||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
||||||
|
|
||||||
| | Linux | macOS | Windows |
|
| | Linux | macOS | Windows |
|
||||||
| :--- | :---: | :---: | :---: |
|
| :--- | :---: | :---: | :---: |
|
||||||
| Chromium <!-- GEN:chromium-version -->134.0.6998.23<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Chromium <!-- GEN:chromium-version -->134.0.6998.35<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| Firefox <!-- GEN:firefox-version -->135.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Firefox <!-- GEN:firefox-version -->135.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,12 @@ Creates new instances of [APIRequestContext].
|
||||||
### option: APIRequest.newContext.extraHTTPHeaders = %%-context-option-extrahttpheaders-%%
|
### option: APIRequest.newContext.extraHTTPHeaders = %%-context-option-extrahttpheaders-%%
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
|
|
||||||
### option: APIRequest.newContext.apiRequestFailsOnErrorStatus = %%-context-option-apiRequestFailsOnErrorStatus-%%
|
### option: APIRequest.newContext.failOnStatusCode
|
||||||
* since: v1.51
|
* since: v1.51
|
||||||
|
- `failOnStatusCode` <[boolean]>
|
||||||
|
|
||||||
|
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
|
||||||
|
for all status codes.
|
||||||
|
|
||||||
### option: APIRequest.newContext.httpCredentials = %%-context-option-httpcredentials-%%
|
### option: APIRequest.newContext.httpCredentials = %%-context-option-httpcredentials-%%
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
|
|
@ -67,25 +71,7 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat
|
||||||
- `localStorage` <[Array]<[Object]>>
|
- `localStorage` <[Array]<[Object]>>
|
||||||
- `name` <[string]>
|
- `name` <[string]>
|
||||||
- `value` <[string]>
|
- `value` <[string]>
|
||||||
- `indexedDB` ?<[Array]<[Object]>> indexedDB to set for context
|
- `indexedDB` ?<[Array]<[unknown]>> indexedDB to set for context
|
||||||
- `name` <[string]> database name
|
|
||||||
- `version` <[int]> database version
|
|
||||||
- `stores` <[Array]<[Object]>>
|
|
||||||
- `name` <[string]>
|
|
||||||
- `keyPath` ?<[string]>
|
|
||||||
- `keyPathArray` ?<[Array]<[string]>>
|
|
||||||
- `autoIncrement` <[boolean]>
|
|
||||||
- `indexes` <[Array]<[Object]>>
|
|
||||||
- `name` <[string]>
|
|
||||||
- `keyPath` ?<[string]>
|
|
||||||
- `keyPathArray` ?<[Array]<[string]>>
|
|
||||||
- `unique` <[boolean]>
|
|
||||||
- `multiEntry` <[boolean]>
|
|
||||||
- `records` <[Array]<[Object]>>
|
|
||||||
- `key` ?<[Object]>
|
|
||||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
- `value` ?<[Object]>
|
|
||||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
|
|
||||||
Populates context with given storage state. This option can be used to initialize context with logged-in information
|
Populates context with given storage state. This option can be used to initialize context with logged-in information
|
||||||
obtained via [`method: BrowserContext.storageState`] or [`method: APIRequestContext.storageState`]. Either a path to the
|
obtained via [`method: BrowserContext.storageState`] or [`method: APIRequestContext.storageState`]. Either a path to the
|
||||||
|
|
|
||||||
|
|
@ -880,25 +880,7 @@ context cookies from the response. The method will automatically follow redirect
|
||||||
- `localStorage` <[Array]<[Object]>>
|
- `localStorage` <[Array]<[Object]>>
|
||||||
- `name` <[string]>
|
- `name` <[string]>
|
||||||
- `value` <[string]>
|
- `value` <[string]>
|
||||||
- `indexedDB` <[Array]<[Object]>>
|
- `indexedDB` <[Array]<[unknown]>>
|
||||||
- `name` <[string]>
|
|
||||||
- `version` <[int]>
|
|
||||||
- `stores` <[Array]<[Object]>>
|
|
||||||
- `name` <[string]>
|
|
||||||
- `keyPath` ?<[string]>
|
|
||||||
- `keyPathArray` ?<[Array]<[string]>>
|
|
||||||
- `autoIncrement` <[boolean]>
|
|
||||||
- `indexes` <[Array]<[Object]>>
|
|
||||||
- `name` <[string]>
|
|
||||||
- `keyPath` ?<[string]>
|
|
||||||
- `keyPathArray` ?<[Array]<[string]>>
|
|
||||||
- `unique` <[boolean]>
|
|
||||||
- `multiEntry` <[boolean]>
|
|
||||||
- `records` <[Array]<[Object]>>
|
|
||||||
- `key` ?<[Object]>
|
|
||||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
- `value` ?<[Object]>
|
|
||||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
|
|
||||||
Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to the constructor.
|
Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to the constructor.
|
||||||
|
|
||||||
|
|
@ -914,4 +896,4 @@ Returns storage state for this request context, contains current cookies and loc
|
||||||
* since: v1.51
|
* since: v1.51
|
||||||
- `indexedDB` ?<boolean>
|
- `indexedDB` ?<boolean>
|
||||||
|
|
||||||
Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
Set to `true` to include IndexedDB in the storage state snapshot.
|
||||||
|
|
|
||||||
|
|
@ -1511,32 +1511,10 @@ Whether to emulate network being offline for the browser context.
|
||||||
- `localStorage` <[Array]<[Object]>>
|
- `localStorage` <[Array]<[Object]>>
|
||||||
- `name` <[string]>
|
- `name` <[string]>
|
||||||
- `value` <[string]>
|
- `value` <[string]>
|
||||||
- `indexedDB` <[Array]<[Object]>>
|
- `indexedDB` <[Array]<[unknown]>>
|
||||||
- `name` <[string]>
|
|
||||||
- `version` <[int]>
|
|
||||||
- `stores` <[Array]<[Object]>>
|
|
||||||
- `name` <[string]>
|
|
||||||
- `keyPath` ?<[string]>
|
|
||||||
- `keyPathArray` ?<[Array]<[string]>>
|
|
||||||
- `autoIncrement` <[boolean]>
|
|
||||||
- `indexes` <[Array]<[Object]>>
|
|
||||||
- `name` <[string]>
|
|
||||||
- `keyPath` ?<[string]>
|
|
||||||
- `keyPathArray` ?<[Array]<[string]>>
|
|
||||||
- `unique` <[boolean]>
|
|
||||||
- `multiEntry` <[boolean]>
|
|
||||||
- `records` <[Array]<[Object]>>
|
|
||||||
- `key` ?<[Object]>
|
|
||||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
- `value` ?<[Object]>
|
|
||||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
|
|
||||||
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
|
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
|
||||||
|
|
||||||
:::note
|
|
||||||
IndexedDBs with typed arrays are currently not supported.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## async method: BrowserContext.storageState
|
## async method: BrowserContext.storageState
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
* langs: csharp, java
|
* langs: csharp, java
|
||||||
|
|
@ -1549,7 +1527,12 @@ IndexedDBs with typed arrays are currently not supported.
|
||||||
* since: v1.51
|
* since: v1.51
|
||||||
- `indexedDB` ?<boolean>
|
- `indexedDB` ?<boolean>
|
||||||
|
|
||||||
Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
Set to `true` to include IndexedDB in the storage state snapshot.
|
||||||
|
If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
IndexedDBs with typed arrays are currently not supported.
|
||||||
|
:::
|
||||||
|
|
||||||
## property: BrowserContext.tracing
|
## property: BrowserContext.tracing
|
||||||
* since: v1.12
|
* since: v1.12
|
||||||
|
|
|
||||||
|
|
@ -1090,6 +1090,9 @@ await rowLocator
|
||||||
### option: Locator.filter.hasNotText = %%-locator-option-has-not-text-%%
|
### option: Locator.filter.hasNotText = %%-locator-option-has-not-text-%%
|
||||||
* since: v1.33
|
* since: v1.33
|
||||||
|
|
||||||
|
### option: Locator.filter.visible = %%-locator-option-visible-%%
|
||||||
|
* since: v1.51
|
||||||
|
|
||||||
## method: Locator.first
|
## method: Locator.first
|
||||||
* since: v1.14
|
* since: v1.14
|
||||||
- returns: <[Locator]>
|
- returns: <[Locator]>
|
||||||
|
|
@ -2332,7 +2335,7 @@ This method expects [Locator] to point to an
|
||||||
## async method: Locator.tap
|
## async method: Locator.tap
|
||||||
* since: v1.14
|
* since: v1.14
|
||||||
|
|
||||||
Perform a tap gesture on the element matching the locator.
|
Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually dispatching touch events, see the [emulating legacy touch events](../touch-events.md) page.
|
||||||
|
|
||||||
**Details**
|
**Details**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2263,13 +2263,13 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
|
||||||
|
|
||||||
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
|
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
|
||||||
|
|
||||||
Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate` and/or `snapshotPathTemplate` properties in the configuration file.
|
Snapshot is stored in a separate `.snapshot.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate` and/or `snapshotPathTemplate` properties in the configuration file.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot();
|
await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.snapshot.yml' });
|
||||||
```
|
```
|
||||||
|
|
||||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
|
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,18 @@ Ensures the page is navigated to the given URL.
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await expect(page).toHaveURL(/.*checkout/);
|
// Check for the page URL to be 'https://playwright.dev/docs/intro' (including query string)
|
||||||
|
await expect(page).toHaveURL('https://playwright.dev/docs/intro');
|
||||||
|
|
||||||
|
// Check for the page URL to contain 'doc', followed by an optional 's', followed by '/'
|
||||||
|
await expect(page).toHaveURL(/docs?\//);
|
||||||
|
|
||||||
|
// Check for the predicate to be satisfied
|
||||||
|
// For example: verify query strings
|
||||||
|
await expect(page).toHaveURL(url => {
|
||||||
|
const params = url.searchParams;
|
||||||
|
return params.has('search') && params.has('options') && params.get('id') === '5';
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|
@ -328,7 +339,7 @@ await Expect(Page).ToHaveURLAsync(new Regex(".*checkout"));
|
||||||
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
|
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
|
||||||
|
|
||||||
Expected URL string, RegExp, or predicate receiving [URL] to match.
|
Expected URL string, RegExp, or predicate receiving [URL] to match.
|
||||||
When a [`option: Browser.newContext.baseURL`] via the context options was provided and the passed URL is a path, it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
When [`option: Browser.newContext.baseURL`] is provided via the context options and the `url` argument is a string, the two values are merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor and used for the comparison against the current browser URL.
|
||||||
|
|
||||||
### option: PageAssertions.toHaveURL.ignoreCase
|
### option: PageAssertions.toHaveURL.ignoreCase
|
||||||
* since: v1.44
|
* since: v1.44
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
|
The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
|
||||||
touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true.
|
touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true.
|
||||||
|
|
||||||
|
This class is limited to emulating tap gestures. For examples of other gestures simulated by manually dispatching touch events, see the [emulating legacy touch events](../touch-events.md) page.
|
||||||
|
|
||||||
## async method: Touchscreen.tap
|
## async method: Touchscreen.tap
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -264,25 +264,7 @@ Specify environment variables that will be visible to the browser. Defaults to `
|
||||||
- `localStorage` <[Array]<[Object]>> localStorage to set for context
|
- `localStorage` <[Array]<[Object]>> localStorage to set for context
|
||||||
- `name` <[string]>
|
- `name` <[string]>
|
||||||
- `value` <[string]>
|
- `value` <[string]>
|
||||||
- `indexedDB` ?<[Array]<[Object]>> indexedDB to set for context
|
- `indexedDB` ?<[Array]<[unknown]>> indexedDB to set for context
|
||||||
- `name` <[string]> database name
|
|
||||||
- `version` <[int]> database version
|
|
||||||
- `stores` <[Array]<[Object]>>
|
|
||||||
- `name` <[string]>
|
|
||||||
- `keyPath` ?<[string]>
|
|
||||||
- `keyPathArray` ?<[Array]<[string]>>
|
|
||||||
- `autoIncrement` <[boolean]>
|
|
||||||
- `indexes` <[Array]<[Object]>>
|
|
||||||
- `name` <[string]>
|
|
||||||
- `keyPath` ?<[string]>
|
|
||||||
- `keyPathArray` ?<[Array]<[string]>>
|
|
||||||
- `unique` <[boolean]>
|
|
||||||
- `multiEntry` <[boolean]>
|
|
||||||
- `records` <[Array]<[Object]>>
|
|
||||||
- `key` ?<[Object]>
|
|
||||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
- `value` ?<[Object]>
|
|
||||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
|
|
||||||
Learn more about [storage state and auth](../auth.md).
|
Learn more about [storage state and auth](../auth.md).
|
||||||
|
|
||||||
|
|
@ -639,11 +621,6 @@ A list of permissions to grant to all pages in this context. See
|
||||||
|
|
||||||
An object containing additional HTTP headers to be sent with every request. Defaults to none.
|
An object containing additional HTTP headers to be sent with every request. Defaults to none.
|
||||||
|
|
||||||
## context-option-apiRequestFailsOnErrorStatus
|
|
||||||
- `apiRequestFailsOnErrorStatus` <[boolean]>
|
|
||||||
|
|
||||||
An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By default, response object is returned for all status codes.
|
|
||||||
|
|
||||||
## context-option-offline
|
## context-option-offline
|
||||||
- `offline` <[boolean]>
|
- `offline` <[boolean]>
|
||||||
|
|
||||||
|
|
@ -1001,7 +978,6 @@ between the same pixel in compared images, between zero (strict) and one (lax),
|
||||||
- %%-context-option-locale-%%
|
- %%-context-option-locale-%%
|
||||||
- %%-context-option-permissions-%%
|
- %%-context-option-permissions-%%
|
||||||
- %%-context-option-extrahttpheaders-%%
|
- %%-context-option-extrahttpheaders-%%
|
||||||
- %%-context-option-apiRequestFailsOnErrorStatus-%%
|
|
||||||
- %%-context-option-offline-%%
|
- %%-context-option-offline-%%
|
||||||
- %%-context-option-httpcredentials-%%
|
- %%-context-option-httpcredentials-%%
|
||||||
- %%-context-option-colorscheme-%%
|
- %%-context-option-colorscheme-%%
|
||||||
|
|
@ -1179,6 +1155,11 @@ Note that outer and inner locators must belong to the same frame. Inner locator
|
||||||
|
|
||||||
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element. When passed a [string], matching is case-insensitive and searches for a substring.
|
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element. When passed a [string], matching is case-insensitive and searches for a substring.
|
||||||
|
|
||||||
|
## locator-option-visible
|
||||||
|
- `visible` <[boolean]>
|
||||||
|
|
||||||
|
Only matches visible or invisible elements.
|
||||||
|
|
||||||
## locator-options-list-v1.14
|
## locator-options-list-v1.14
|
||||||
- %%-locator-option-has-text-%%
|
- %%-locator-option-has-text-%%
|
||||||
- %%-locator-option-has-%%
|
- %%-locator-option-has-%%
|
||||||
|
|
|
||||||
|
|
@ -339,10 +339,10 @@ npx playwright test --update-snapshots --update-source-mode=3way
|
||||||
|
|
||||||
#### Snapshots as separate files
|
#### Snapshots as separate files
|
||||||
|
|
||||||
To store your snapshots in a separate file, use the `toMatchAriaSnapshot` method with the `name` option, specifying a `.yml` file extension.
|
To store your snapshots in a separate file, use the `toMatchAriaSnapshot` method with the `name` option, specifying a `.snapshot.yml` file extension.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await expect(page.getByRole('main')).toMatchAriaSnapshot({ name: 'main-snapshot.yml' });
|
await expect(page.getByRole('main')).toMatchAriaSnapshot({ name: 'main.snapshot.yml' });
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, snapshots from a test file `example.spec.ts` are placed in the `example.spec.ts-snapshots` directory. As snapshots should be the same across browsers, only one snapshot is saved even if testing with multiple browsers. Should you wish, you can customize the [snapshot path template](./api/class-testconfig#test-config-snapshot-path-template) using the following configuration:
|
By default, snapshots from a test file `example.spec.ts` are placed in the `example.spec.ts-snapshots` directory. As snapshots should be the same across browsers, only one snapshot is saved even if testing with multiple browsers. Should you wish, you can customize the [snapshot path template](./api/class-testconfig#test-config-snapshot-path-template) using the following configuration:
|
||||||
|
|
|
||||||
|
|
@ -751,10 +751,10 @@ page.locator("x-details", new Page.LocatorOptions().setHasText("Details"))
|
||||||
.click();
|
.click();
|
||||||
```
|
```
|
||||||
```python async
|
```python async
|
||||||
await page.locator("x-details", has_text="Details" ).click()
|
await page.locator("x-details", has_text="Details").click()
|
||||||
```
|
```
|
||||||
```python sync
|
```python sync
|
||||||
page.locator("x-details", has_text="Details" ).click()
|
page.locator("x-details", has_text="Details").click()
|
||||||
```
|
```
|
||||||
```csharp
|
```csharp
|
||||||
await page
|
await page
|
||||||
|
|
@ -1310,19 +1310,19 @@ Consider a page with two buttons, the first invisible and the second [visible](.
|
||||||
* This will only find a second button, because it is visible, and then click it.
|
* This will only find a second button, because it is visible, and then click it.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await page.locator('button').locator('visible=true').click();
|
await page.locator('button').filter({ visible: true }).click();
|
||||||
```
|
```
|
||||||
```java
|
```java
|
||||||
page.locator("button").locator("visible=true").click();
|
page.locator("button").filter(new Locator.FilterOptions.setVisible(true)).click();
|
||||||
```
|
```
|
||||||
```python async
|
```python async
|
||||||
await page.locator("button").locator("visible=true").click()
|
await page.locator("button").filter(visible=True).click()
|
||||||
```
|
```
|
||||||
```python sync
|
```python sync
|
||||||
page.locator("button").locator("visible=true").click()
|
page.locator("button").filter(visible=True).click()
|
||||||
```
|
```
|
||||||
```csharp
|
```csharp
|
||||||
await page.Locator("button").Locator("visible=true").ClickAsync();
|
await page.Locator("button").Filter(new() { Visible = true }).ClickAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lists
|
## Lists
|
||||||
|
|
|
||||||
|
|
@ -708,9 +708,13 @@ Playwright uses simplified glob patterns for URL matching in network interceptio
|
||||||
- A double `**` matches any characters including `/`
|
- A double `**` matches any characters including `/`
|
||||||
1. Question mark `?` matches any single character except `/`
|
1. Question mark `?` matches any single character except `/`
|
||||||
1. Curly braces `{}` can be used to match a list of options separated by commas `,`
|
1. Curly braces `{}` can be used to match a list of options separated by commas `,`
|
||||||
|
1. Square brackets `[]` can be used to match a set of characters
|
||||||
|
1. Backslash `\` can be used to escape any of special characters (note to escape backslash itself as `\\`)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- `https://example.com/*.js` matches `https://example.com/file.js` but not `https://example.com/path/file.js`
|
- `https://example.com/*.js` matches `https://example.com/file.js` but not `https://example.com/path/file.js`
|
||||||
|
- `https://example.com/\\?page=1` matches `https://example.com/?page=1` but not `https://example.com`
|
||||||
|
- `**/v[0-9]*` matches `https://example.com/v1/` but not `https://example.com/vote/`
|
||||||
- `**/*.js` matches both `https://example.com/file.js` and `https://example.com/path/file.js`
|
- `**/*.js` matches both `https://example.com/file.js` and `https://example.com/path/file.js`
|
||||||
- `**/*.{png,jpg,jpeg}` matches all image requests
|
- `**/*.{png,jpg,jpeg}` matches all image requests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ See [`property: TestConfig.reporter`].
|
||||||
## property: FullConfig.reportSlowTests
|
## property: FullConfig.reportSlowTests
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: <[null]|[Object]>
|
- type: <[null]|[Object]>
|
||||||
- `max` <[int]> The maximum number of slow test files to report. Defaults to `5`.
|
- `max` <[int]> The maximum number of slow test files to report.
|
||||||
- `threshold` <[float]> Test duration in milliseconds that is considered slow. Defaults to 15 seconds.
|
- `threshold` <[float]> Test file duration in milliseconds that is considered slow.
|
||||||
|
|
||||||
See [`property: TestConfig.reportSlowTests`].
|
See [`property: TestConfig.reportSlowTests`].
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,10 @@ export default defineConfig({
|
||||||
|
|
||||||
Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as key-value pairs, and JSON report will include metadata serialized as json.
|
Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as key-value pairs, and JSON report will include metadata serialized as json.
|
||||||
|
|
||||||
See also [`property: TestConfig.populateGitInfo`] that populates metadata.
|
* Providing `gitCommit: 'generate'` property will populate it with the git commit details.
|
||||||
|
* Providing `gitDiff: 'generate'` property will populate it with the git diff details.
|
||||||
|
|
||||||
|
On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic generation.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
|
|
@ -326,26 +329,6 @@ This path will serve as the base directory for each test file snapshot directory
|
||||||
## property: TestConfig.snapshotPathTemplate = %%-test-config-snapshot-path-template-%%
|
## property: TestConfig.snapshotPathTemplate = %%-test-config-snapshot-path-template-%%
|
||||||
* since: v1.28
|
* since: v1.28
|
||||||
|
|
||||||
## property: TestConfig.populateGitInfo
|
|
||||||
* since: v1.51
|
|
||||||
- type: ?<[boolean]>
|
|
||||||
|
|
||||||
Whether to populate `'git.commit.info'` field of the [`property: TestConfig.metadata`] with Git commit info and CI/CD information.
|
|
||||||
|
|
||||||
This information will appear in the HTML and JSON reports and is available in the Reporter API.
|
|
||||||
|
|
||||||
On Github Actions, this feature is enabled by default.
|
|
||||||
|
|
||||||
**Usage**
|
|
||||||
|
|
||||||
```js title="playwright.config.ts"
|
|
||||||
import { defineConfig } from '@playwright/test';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
populateGitInfo: !!process.env.CI,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## property: TestConfig.preserveOutput
|
## property: TestConfig.preserveOutput
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[PreserveOutput]<"always"|"never"|"failures-only">>
|
- type: ?<[PreserveOutput]<"always"|"never"|"failures-only">>
|
||||||
|
|
@ -450,7 +433,7 @@ export default defineConfig({
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[null]|[Object]>
|
- type: ?<[null]|[Object]>
|
||||||
- `max` <[int]> The maximum number of slow test files to report. Defaults to `5`.
|
- `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.
|
- `threshold` <[float]> Test file duration in milliseconds that is considered slow. Defaults to 5 minutes.
|
||||||
|
|
||||||
Whether to report slow test files. Pass `null` to disable this feature.
|
Whether to report slow test files. Pass `null` to disable this feature.
|
||||||
|
|
||||||
|
|
@ -684,7 +667,7 @@ import { defineConfig } from '@playwright/test';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
url: 'http://127.0.0.1:3000',
|
url: 'http://localhost:3000',
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
|
|
@ -713,19 +696,19 @@ export default defineConfig({
|
||||||
webServer: [
|
webServer: [
|
||||||
{
|
{
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
url: 'http://127.0.0.1:3000',
|
url: 'http://localhost:3000',
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: 'npm run backend',
|
command: 'npm run backend',
|
||||||
url: 'http://127.0.0.1:3333',
|
url: 'http://localhost:3333',
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://127.0.0.1:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export default defineConfig({
|
||||||
|
|
||||||
use: {
|
use: {
|
||||||
// Base URL to use in actions like `await page.goto('/')`.
|
// Base URL to use in actions like `await page.goto('/')`.
|
||||||
baseURL: 'http://127.0.0.1:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
|
|
||||||
// Collect trace when retrying the failed test.
|
// Collect trace when retrying the failed test.
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
|
|
@ -50,7 +50,7 @@ export default defineConfig({
|
||||||
// Run your local dev server before starting the tests.
|
// Run your local dev server before starting the tests.
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
url: 'http://127.0.0.1:3000',
|
url: 'http://localhost:3000',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { defineConfig } from '@playwright/test';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
use: {
|
use: {
|
||||||
// Base URL to use in actions like `await page.goto('/')`.
|
// Base URL to use in actions like `await page.goto('/')`.
|
||||||
baseURL: 'http://127.0.0.1:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
|
|
||||||
// Populates context with given storage state.
|
// Populates context with given storage state.
|
||||||
storageState: 'state.json',
|
storageState: 'state.json',
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export default defineConfig({
|
||||||
// Run your local dev server before starting the tests
|
// Run your local dev server before starting the tests
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
url: 'http://127.0.0.1:3000',
|
url: 'http://localhost:3000',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
stdout: 'ignore',
|
stdout: 'ignore',
|
||||||
stderr: 'pipe',
|
stderr: 'pipe',
|
||||||
|
|
@ -52,7 +52,7 @@ export default defineConfig({
|
||||||
// Run your local dev server before starting the tests
|
// Run your local dev server before starting the tests
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
url: 'http://127.0.0.1:3000',
|
url: 'http://localhost:3000',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
},
|
},
|
||||||
|
|
@ -63,7 +63,7 @@ export default defineConfig({
|
||||||
|
|
||||||
It is also recommended to specify the `baseURL` in the `use: {}` section of your config, so that tests can use relative urls and you don't have to specify the full URL over and over again.
|
It is also recommended to specify the `baseURL` in the `use: {}` section of your config, so that tests can use relative urls and you don't have to specify the full URL over and over again.
|
||||||
|
|
||||||
When using [`method: Page.goto`], [`method: Page.route`], [`method: Page.waitForURL`], [`method: Page.waitForRequest`], or [`method: Page.waitForResponse`] it takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. For Example, by setting the baseURL to `http://127.0.0.1:3000` and navigating to `/login` in your tests, Playwright will run the test using `http://127.0.0.1:3000/login`.
|
When using [`method: Page.goto`], [`method: Page.route`], [`method: Page.waitForURL`], [`method: Page.waitForRequest`], or [`method: Page.waitForResponse`] it takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. For Example, by setting the baseURL to `http://localhost:3000` and navigating to `/login` in your tests, Playwright will run the test using `http://localhost:3000/login`.
|
||||||
|
|
||||||
```js title="playwright.config.ts"
|
```js title="playwright.config.ts"
|
||||||
import { defineConfig } from '@playwright/test';
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
@ -74,11 +74,11 @@ export default defineConfig({
|
||||||
// Run your local dev server before starting the tests
|
// Run your local dev server before starting the tests
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
url: 'http://127.0.0.1:3000',
|
url: 'http://localhost:3000',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://127.0.0.1:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -89,7 +89,7 @@ Now you can use a relative path when navigating the page:
|
||||||
import { test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
|
|
||||||
test('test', async ({ page }) => {
|
test('test', async ({ page }) => {
|
||||||
// This will navigate to http://127.0.0.1:3000/login
|
// This will navigate to http://localhost:3000/login
|
||||||
await page.goto('./login');
|
await page.goto('./login');
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -106,19 +106,19 @@ export default defineConfig({
|
||||||
webServer: [
|
webServer: [
|
||||||
{
|
{
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
url: 'http://127.0.0.1:3000',
|
url: 'http://localhost:3000',
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: 'npm run backend',
|
command: 'npm run backend',
|
||||||
url: 'http://127.0.0.1:3333',
|
url: 'http://localhost:3333',
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://127.0.0.1:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,13 @@
|
||||||
---
|
---
|
||||||
id: touch-events
|
id: touch-events
|
||||||
title: "Emulating touch events"
|
title: "Emulating legacy touch events"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Mobile web sites may listen to [touch events](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events) and react to user touch gestures such as swipe, pinch, tap etc. To test this functionality you can manually generate [TouchEvent]s in the page context using [`method: Locator.evaluate`].
|
Web applications that handle [touch events](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events) to respond to gestures like swipe, pinch, and tap can be tested by manually dispatching [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent)s to the page. The examples below demonstrate how to use [`method: Locator.dispatchEvent`] and pass [Touch](https://developer.mozilla.org/en-US/docs/Web/API/Touch) points as arguments.
|
||||||
|
|
||||||
If your web application relies on [pointer events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) instead of touch events, you can use [`method: Locator.click`] and raw [`Mouse`] events to simulate a single-finger touch, and this will trigger all the same pointer events.
|
### Emulating pan gesture
|
||||||
|
|
||||||
### Dispatching TouchEvent
|
|
||||||
|
|
||||||
You can dispatch touch events to the page using [`method: Locator.dispatchEvent`]. [Touch](https://developer.mozilla.org/en-US/docs/Web/API/Touch) points can be passed as arguments, see examples below.
|
|
||||||
|
|
||||||
#### Emulating pan gesture
|
|
||||||
|
|
||||||
In the example below, we emulate pan gesture that is expected to move the map. The app under test only uses `clientX/clientY` coordinates of the touch point, so we initialize just that. In a more complex scenario you may need to also set `pageX/pageY/screenX/screenY`, if your app needs them.
|
In the example below, we emulate pan gesture that is expected to move the map. The app under test only uses `clientX/clientY` coordinates of the touch point, so we initialize just that. In a more complex scenario you may need to also set `pageX/pageY/screenX/screenY`, if your app needs them.
|
||||||
|
|
||||||
|
|
@ -69,7 +63,7 @@ test(`pan gesture to move the map`, async ({ page }) => {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Emulating pinch gesture
|
### Emulating pinch gesture
|
||||||
|
|
||||||
In the example below, we emulate pinch gesture, i.e. two touch points moving closer to each other. It is expected to zoom out the map. The app under test only uses `clientX/clientY` coordinates of touch points, so we initialize just that. In a more complex scenario you may need to also set `pageX/pageY/screenX/screenY`, if your app needs them.
|
In the example below, we emulate pinch gesture, i.e. two touch points moving closer to each other. It is expected to zoom out the map. The app under test only uses `clientX/clientY` coordinates of touch points, so we initialize just that. In a more complex scenario you may need to also set `pageX/pageY/screenX/screenY`, if your app needs them.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,11 @@ export default [{
|
||||||
'no-console': 'off'
|
'no-console': 'off'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
files: ['packages/playwright-core/src/server/injected/**/*.ts'],
|
files: [
|
||||||
|
'packages/playwright-core/src/server/injected/**/*.ts',
|
||||||
|
'packages/playwright-core/src/server/isomorphic/**/*.ts',
|
||||||
|
'packages/playwright-core/src/utils/isomorphic/**/*.ts',
|
||||||
|
],
|
||||||
languageOptions: languageOptionsWithTsConfig,
|
languageOptions: languageOptionsWithTsConfig,
|
||||||
rules: {
|
rules: {
|
||||||
...noWebGlobalsRules,
|
...noWebGlobalsRules,
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,6 @@
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-section {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-properties {
|
.metadata-properties {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -57,9 +53,8 @@
|
||||||
border-bottom: 1px solid var(--color-border-default);
|
border-bottom: 1px solid var(--color-border-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.git-commit-info a {
|
.metadata-view a {
|
||||||
color: var(--color-fg-default);
|
color: var(--color-fg-default);
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyable-property {
|
.copyable-property {
|
||||||
|
|
|
||||||
|
|
@ -20,32 +20,10 @@ import './common.css';
|
||||||
import './theme.css';
|
import './theme.css';
|
||||||
import './metadataView.css';
|
import './metadataView.css';
|
||||||
import type { Metadata } from '@playwright/test';
|
import type { Metadata } from '@playwright/test';
|
||||||
import type { GitCommitInfo } from '@testIsomorphic/types';
|
import type { CIInfo, GitCommitInfo, MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||||
import { CopyToClipboardContainer } from './copyToClipboard';
|
import { CopyToClipboardContainer } from './copyToClipboard';
|
||||||
import { linkifyText } from '@web/renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
|
|
||||||
type MetadataEntries = [string, unknown][];
|
|
||||||
|
|
||||||
export const MetadataContext = React.createContext<MetadataEntries>([]);
|
|
||||||
|
|
||||||
export function MetadataProvider({ metadata, children }: React.PropsWithChildren<{ metadata: Metadata }>) {
|
|
||||||
const entries = React.useMemo(() => {
|
|
||||||
// TODO: do not plumb actualWorkers through metadata.
|
|
||||||
return Object.entries(metadata).filter(([key]) => key !== 'actualWorkers');
|
|
||||||
}, [metadata]);
|
|
||||||
|
|
||||||
return <MetadataContext.Provider value={entries}>{children}</MetadataContext.Provider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMetadata() {
|
|
||||||
return React.useContext(MetadataContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGitCommitInfo() {
|
|
||||||
const metadataEntries = useMetadata();
|
|
||||||
return metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error: Error | null, errorInfo: React.ErrorInfo | null }> {
|
class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error: Error | null, errorInfo: React.ErrorInfo | null }> {
|
||||||
override state: { error: Error | null, errorInfo: React.ErrorInfo | null } = {
|
override state: { error: Error | null, errorInfo: React.ErrorInfo | null } = {
|
||||||
error: null,
|
error: null,
|
||||||
|
|
@ -72,27 +50,26 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetadataView = () => {
|
export const MetadataView: React.FC<{ metadata: Metadata }> = params => {
|
||||||
return <ErrorBoundary><InnerMetadataView/></ErrorBoundary>;
|
return <ErrorBoundary><InnerMetadataView metadata={params.metadata}/></ErrorBoundary>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InnerMetadataView = () => {
|
const InnerMetadataView: React.FC<{ metadata: Metadata }> = params => {
|
||||||
const metadataEntries = useMetadata();
|
const commitInfo = params.metadata as MetadataWithCommitInfo;
|
||||||
const gitCommitInfo = useGitCommitInfo();
|
const otherEntries = Object.entries(params.metadata).filter(([key]) => !ignoreKeys.has(key));
|
||||||
const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
|
const hasMetadata = commitInfo.ci || commitInfo.gitCommit || otherEntries.length > 0;
|
||||||
if (!gitCommitInfo && !entries.length)
|
if (!hasMetadata)
|
||||||
return null;
|
return;
|
||||||
return <div className='metadata-view'>
|
return <div className='metadata-view'>
|
||||||
{gitCommitInfo && <>
|
{commitInfo.ci && !commitInfo.gitCommit && <CiInfoView info={commitInfo.ci}/>}
|
||||||
<GitCommitInfoView info={gitCommitInfo}/>
|
{commitInfo.gitCommit && <GitCommitInfoView ci={commitInfo.ci} commit={commitInfo.gitCommit}/>}
|
||||||
{entries.length > 0 && <div className='metadata-separator' />}
|
{otherEntries.length > 0 && (commitInfo.gitCommit || commitInfo.ci) && <div className='metadata-separator' />}
|
||||||
</>}
|
<div className='metadata-section metadata-properties' role='list'>
|
||||||
<div className='metadata-section metadata-properties'>
|
{otherEntries.map(([propertyName, value]) => {
|
||||||
{entries.map(([propertyName, value]) => {
|
|
||||||
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
|
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
|
||||||
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
||||||
return (
|
return (
|
||||||
<div key={propertyName} className='copyable-property'>
|
<div key={propertyName} className='copyable-property' role='listitem'>
|
||||||
<CopyToClipboardContainer value={valueString}>
|
<CopyToClipboardContainer value={valueString}>
|
||||||
<span style={{ fontWeight: 'bold' }} title={propertyName}>{propertyName}</span>
|
<span style={{ fontWeight: 'bold' }} title={propertyName}>{propertyName}</span>
|
||||||
: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span>
|
: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span>
|
||||||
|
|
@ -104,48 +81,39 @@ const InnerMetadataView = () => {
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
const CiInfoView: React.FC<{ info: CIInfo }> = ({ info }) => {
|
||||||
const email = info['revision.email'] ? ` <${info['revision.email']}>` : '';
|
const title = info.prTitle || `Commit ${info.commitHash}`;
|
||||||
const author = `${info['revision.author'] || ''}${email}`;
|
const link = info.prHref || info.commitHref;
|
||||||
|
return <div className='metadata-section' role='list'>
|
||||||
let subject = info['revision.subject'] || '';
|
<div role='listitem'>
|
||||||
let link = info['revision.link'];
|
<a href={link} target='_blank' rel='noopener noreferrer' title={title}>{title}</a>
|
||||||
let shortSubject = info['revision.id']?.slice(0, 7) || 'unknown';
|
|
||||||
|
|
||||||
if (info['pull.link'] && info['pull.title']) {
|
|
||||||
subject = info['pull.title'];
|
|
||||||
link = info['pull.link'];
|
|
||||||
shortSubject = link ? 'Pull Request' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info['revision.timestamp']);
|
|
||||||
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info['revision.timestamp']);
|
|
||||||
return <div className='hbox git-commit-info metadata-section'>
|
|
||||||
<div className='vbox metadata-properties'>
|
|
||||||
<div>
|
|
||||||
{link ? (
|
|
||||||
<a href={link} target='_blank' rel='noopener noreferrer' title={subject}>
|
|
||||||
{subject}
|
|
||||||
</a>
|
|
||||||
) : <span title={subject}>
|
|
||||||
{subject}
|
|
||||||
</span>}
|
|
||||||
</div>
|
|
||||||
<div className='hbox'>
|
|
||||||
<span className='mr-1'>{author}</span>
|
|
||||||
<span title={longTimestamp}> on {shortTimestamp}</span>
|
|
||||||
{info['ci.link'] && (
|
|
||||||
<>
|
|
||||||
<span className='mx-2'>·</span>
|
|
||||||
<a href={info['ci.link']} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>Logs</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{link ? (
|
|
||||||
<a href={link} target='_blank' rel='noopener noreferrer' title='View commit details'>
|
|
||||||
{shortSubject}
|
|
||||||
</a>
|
|
||||||
) : !!shortSubject && <span>{shortSubject}</span>}
|
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GitCommitInfoView: React.FC<{ ci?: CIInfo, commit: GitCommitInfo }> = ({ ci, commit }) => {
|
||||||
|
const title = ci?.prTitle || commit.subject;
|
||||||
|
const link = ci?.prHref || ci?.commitHref;
|
||||||
|
const email = ` <${commit.author.email}>`;
|
||||||
|
const author = `${commit.author.name}${email}`;
|
||||||
|
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(commit.committer.time);
|
||||||
|
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(commit.committer.time);
|
||||||
|
|
||||||
|
return <div className='metadata-section' role='list'>
|
||||||
|
<div role='listitem'>
|
||||||
|
{link && <a href={link} target='_blank' rel='noopener noreferrer' title={title}>{title}</a>}
|
||||||
|
{!link && <span title={title}>{title}</span>}
|
||||||
|
</div>
|
||||||
|
<div role='listitem' className='hbox'>
|
||||||
|
<span className='mr-1'>{author}</span>
|
||||||
|
<span title={longTimestamp}> on {shortTimestamp}</span>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ignoreKeys = new Set(['ci', 'gitCommit', 'gitDiff', 'actualWorkers']);
|
||||||
|
|
||||||
|
export const isMetadataEmpty = (metadata: MetadataWithCommitInfo): boolean => {
|
||||||
|
const otherEntries = Object.entries(metadata).filter(([key]) => !ignoreKeys.has(key));
|
||||||
|
return !metadata.ci && !metadata.gitCommit && !otherEntries.length;
|
||||||
|
};
|
||||||
|
|
|
||||||
29
packages/html-reporter/src/reportContext.tsx
Normal file
29
packages/html-reporter/src/reportContext.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import type { HTMLReport } from './types';
|
||||||
|
|
||||||
|
|
||||||
|
const HTMLReportContext = React.createContext<HTMLReport | undefined>(undefined);
|
||||||
|
|
||||||
|
export function HTMLReportContextProvider({ report, children }: React.PropsWithChildren<{ report: HTMLReport | undefined }>) {
|
||||||
|
return <HTMLReportContext.Provider value={report}>{children}</HTMLReportContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHTMLReport() {
|
||||||
|
return React.useContext(HTMLReportContext);
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ import './reportView.css';
|
||||||
import { TestCaseView } from './testCaseView';
|
import { TestCaseView } from './testCaseView';
|
||||||
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
||||||
import './theme.css';
|
import './theme.css';
|
||||||
import { MetadataProvider } from './metadataView';
|
import { HTMLReportContextProvider } from './reportContext';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
@ -73,7 +73,7 @@ export const ReportView: React.FC<{
|
||||||
return result;
|
return result;
|
||||||
}, [report, filter]);
|
}, [report, filter]);
|
||||||
|
|
||||||
return <MetadataProvider metadata={report?.json().metadata ?? {}}><div className='htmlreport vbox px-4 pb-4'>
|
return <HTMLReportContextProvider report={report?.json()}><div className='htmlreport vbox px-4 pb-4'>
|
||||||
<main>
|
<main>
|
||||||
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||||
<Route predicate={testFilesRoutePredicate}>
|
<Route predicate={testFilesRoutePredicate}>
|
||||||
|
|
@ -89,7 +89,7 @@ export const ReportView: React.FC<{
|
||||||
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
|
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
|
||||||
</Route>
|
</Route>
|
||||||
</main>
|
</main>
|
||||||
</div></MetadataProvider>;
|
</div></HTMLReportContextProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TestCaseViewLoader: React.FC<{
|
const TestCaseViewLoader: React.FC<{
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,14 @@ import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||||
import type { TestResult } from './types';
|
import type { TestResult } from './types';
|
||||||
import { fixTestPrompt } from '@web/components/prompts';
|
import { fixTestPrompt } from '@web/components/prompts';
|
||||||
import { useGitCommitInfo } from './metadataView';
|
import { useHTMLReport } from './reportContext';
|
||||||
|
import type { MetadataWithCommitInfo } from '@playwright/isomorphic/types';
|
||||||
|
|
||||||
export const TestErrorView: React.FC<{ error: string; testId?: string; result?: TestResult }> = ({ error, testId, result }) => {
|
export const TestErrorView: React.FC<{
|
||||||
|
error: string;
|
||||||
|
testId?: string;
|
||||||
|
result?: TestResult
|
||||||
|
}> = ({ error, testId, result }) => {
|
||||||
return (
|
return (
|
||||||
<CodeSnippet code={error} testId={testId}>
|
<CodeSnippet code={error} testId={testId}>
|
||||||
<div style={{ float: 'right', margin: 10 }}>
|
<div style={{ float: 'right', margin: 10 }}>
|
||||||
|
|
@ -47,12 +52,13 @@ const PromptButton: React.FC<{
|
||||||
error: string;
|
error: string;
|
||||||
result?: TestResult;
|
result?: TestResult;
|
||||||
}> = ({ error, result }) => {
|
}> = ({ error, result }) => {
|
||||||
const gitCommitInfo = useGitCommitInfo();
|
const report = useHTMLReport();
|
||||||
|
const commitInfo = report?.metadata as MetadataWithCommitInfo | undefined;
|
||||||
const prompt = React.useMemo(() => fixTestPrompt(
|
const prompt = React.useMemo(() => fixTestPrompt(
|
||||||
error,
|
error,
|
||||||
gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'],
|
commitInfo?.gitDiff,
|
||||||
result?.attachments.find(a => a.name === 'pageSnapshot')?.body
|
result?.attachments.find(a => a.name === 'pageSnapshot')?.body
|
||||||
), [gitCommitInfo, result, error]);
|
), [commitInfo, result, error]);
|
||||||
|
|
||||||
const [copied, setCopied] = React.useState(false);
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import { msToString } from './utils';
|
||||||
import { AutoChip } from './chip';
|
import { AutoChip } from './chip';
|
||||||
import { TestErrorView } from './testErrorView';
|
import { TestErrorView } from './testErrorView';
|
||||||
import * as icons from './icons';
|
import * as icons from './icons';
|
||||||
import { MetadataView, useMetadata } from './metadataView';
|
import { isMetadataEmpty, MetadataView } from './metadataView';
|
||||||
|
|
||||||
export const TestFilesView: React.FC<{
|
export const TestFilesView: React.FC<{
|
||||||
tests: TestFileSummary[],
|
tests: TestFileSummary[],
|
||||||
|
|
@ -67,13 +67,12 @@ export const TestFilesHeader: React.FC<{
|
||||||
metadataVisible: boolean,
|
metadataVisible: boolean,
|
||||||
toggleMetadataVisible: () => void,
|
toggleMetadataVisible: () => void,
|
||||||
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
|
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
|
||||||
const metadataEntries = useMetadata();
|
|
||||||
if (!report)
|
if (!report)
|
||||||
return null;
|
return null;
|
||||||
return <>
|
return <>
|
||||||
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
|
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
|
||||||
<div className='test-file-header-info'>
|
<div className='test-file-header-info'>
|
||||||
{metadataEntries.length > 0 && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
|
{!isMetadataEmpty(report.metadata) && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
|
||||||
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
|
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
|
||||||
</div>}
|
</div>}
|
||||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name'>Project: {report.projectNames[0]}</div>}
|
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name'>Project: {report.projectNames[0]}</div>}
|
||||||
|
|
@ -83,7 +82,7 @@ export const TestFilesHeader: React.FC<{
|
||||||
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
||||||
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
|
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
{metadataVisible && <MetadataView/>}
|
{metadataVisible && <MetadataView metadata={report.metadata}/>}
|
||||||
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
|
|
|
||||||
296
packages/playwright-client/types/types.d.ts
vendored
296
packages/playwright-client/types/types.d.ts
vendored
|
|
@ -9267,14 +9267,15 @@ export interface BrowserContext {
|
||||||
/**
|
/**
|
||||||
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB
|
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB
|
||||||
* snapshot.
|
* snapshot.
|
||||||
*
|
|
||||||
* **NOTE** IndexedDBs with typed arrays are currently not supported.
|
|
||||||
*
|
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
storageState(options?: {
|
storageState(options?: {
|
||||||
/**
|
/**
|
||||||
* Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
* Set to `true` to include IndexedDB in the storage state snapshot. If your application uses IndexedDB to store
|
||||||
|
* authentication tokens, like Firebase Authentication, enable this.
|
||||||
|
*
|
||||||
|
* **NOTE** IndexedDBs with typed arrays are currently not supported.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
indexedDB?: boolean;
|
indexedDB?: boolean;
|
||||||
|
|
||||||
|
|
@ -9316,49 +9317,7 @@ export interface BrowserContext {
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
indexedDB: Array<{
|
indexedDB: Array<unknown>;
|
||||||
name: string;
|
|
||||||
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
@ -9741,12 +9700,6 @@ export interface Browser {
|
||||||
*/
|
*/
|
||||||
acceptDownloads?: boolean;
|
acceptDownloads?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),
|
* When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),
|
||||||
* [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
|
* [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
|
||||||
|
|
@ -10135,55 +10088,7 @@ export interface Browser {
|
||||||
/**
|
/**
|
||||||
* indexedDB to set for context
|
* indexedDB to set for context
|
||||||
*/
|
*/
|
||||||
indexedDB?: Array<{
|
indexedDB?: Array<unknown>;
|
||||||
/**
|
|
||||||
* database name
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* database version
|
|
||||||
*/
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -13224,6 +13129,11 @@ export interface Locator {
|
||||||
* `<article><div>Playwright</div></article>`.
|
* `<article><div>Playwright</div></article>`.
|
||||||
*/
|
*/
|
||||||
hasText?: string|RegExp;
|
hasText?: string|RegExp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only matches visible or invisible elements.
|
||||||
|
*/
|
||||||
|
visible?: boolean;
|
||||||
}): Locator;
|
}): Locator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -14426,7 +14336,8 @@ export interface Locator {
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a tap gesture on the element matching the locator.
|
* Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
|
||||||
|
* dispatching touch events, see the [emulating legacy touch events](https://playwright.dev/docs/touch-events) page.
|
||||||
*
|
*
|
||||||
* **Details**
|
* **Details**
|
||||||
*
|
*
|
||||||
|
|
@ -14812,12 +14723,6 @@ export interface BrowserType<Unused = {}> {
|
||||||
*/
|
*/
|
||||||
acceptDownloads?: boolean;
|
acceptDownloads?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
|
* **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
|
||||||
*
|
*
|
||||||
|
|
@ -16708,12 +16613,6 @@ export interface AndroidDevice {
|
||||||
*/
|
*/
|
||||||
acceptDownloads?: boolean;
|
acceptDownloads?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
|
* **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
|
||||||
*
|
*
|
||||||
|
|
@ -17560,12 +17459,6 @@ export interface APIRequest {
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
newContext(options?: {
|
newContext(options?: {
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Methods like
|
* Methods like
|
||||||
* [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get)
|
* [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get)
|
||||||
|
|
@ -17641,6 +17534,12 @@ export interface APIRequest {
|
||||||
*/
|
*/
|
||||||
extraHTTPHeaders?: { [key: string]: string; };
|
extraHTTPHeaders?: { [key: string]: string; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status
|
||||||
|
* codes.
|
||||||
|
*/
|
||||||
|
failOnStatusCode?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no
|
* Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no
|
||||||
* origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
* origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||||
|
|
@ -17742,55 +17641,7 @@ export interface APIRequest {
|
||||||
/**
|
/**
|
||||||
* indexedDB to set for context
|
* indexedDB to set for context
|
||||||
*/
|
*/
|
||||||
indexedDB?: Array<{
|
indexedDB?: Array<unknown>;
|
||||||
/**
|
|
||||||
* database name
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* database version
|
|
||||||
*/
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18564,7 +18415,7 @@ export interface APIRequestContext {
|
||||||
*/
|
*/
|
||||||
storageState(options?: {
|
storageState(options?: {
|
||||||
/**
|
/**
|
||||||
* Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
* Set to `true` to include IndexedDB in the storage state snapshot.
|
||||||
*/
|
*/
|
||||||
indexedDB?: boolean;
|
indexedDB?: boolean;
|
||||||
|
|
||||||
|
|
@ -18606,49 +18457,7 @@ export interface APIRequestContext {
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
indexedDB: Array<{
|
indexedDB: Array<unknown>;
|
||||||
name: string;
|
|
||||||
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
@ -21310,6 +21119,9 @@ export interface Selectors {
|
||||||
/**
|
/**
|
||||||
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on
|
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on
|
||||||
* the touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true.
|
* the touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true.
|
||||||
|
*
|
||||||
|
* This class is limited to emulating tap gestures. For examples of other gestures simulated by manually dispatching
|
||||||
|
* touch events, see the [emulating legacy touch events](https://playwright.dev/docs/touch-events) page.
|
||||||
*/
|
*/
|
||||||
export interface Touchscreen {
|
export interface Touchscreen {
|
||||||
/**
|
/**
|
||||||
|
|
@ -22142,12 +21954,6 @@ export interface BrowserContextOptions {
|
||||||
*/
|
*/
|
||||||
acceptDownloads?: boolean;
|
acceptDownloads?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),
|
* When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),
|
||||||
* [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
|
* [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
|
||||||
|
|
@ -22503,55 +22309,7 @@ export interface BrowserContextOptions {
|
||||||
/**
|
/**
|
||||||
* indexedDB to set for context
|
* indexedDB to set for context
|
||||||
*/
|
*/
|
||||||
indexedDB?: Array<{
|
indexedDB?: Array<unknown>;
|
||||||
/**
|
|
||||||
* database name
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* database version
|
|
||||||
*/
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@
|
||||||
"browsers": [
|
"browsers": [
|
||||||
{
|
{
|
||||||
"name": "chromium",
|
"name": "chromium",
|
||||||
"revision": "1160",
|
"revision": "1161",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "134.0.6998.23"
|
"browserVersion": "134.0.6998.35"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-headless-shell",
|
"name": "chromium-headless-shell",
|
||||||
"revision": "1160",
|
"revision": "1161",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "134.0.6998.23"
|
"browserVersion": "134.0.6998.35"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-tip-of-tree",
|
"name": "chromium-tip-of-tree",
|
||||||
|
|
|
||||||
|
|
@ -510,7 +510,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
|
|
||||||
async function prepareStorageState(platform: Platform, options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
|
async function prepareStorageState(platform: Platform, options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
|
||||||
if (typeof options.storageState !== 'string')
|
if (typeof options.storageState !== 'string')
|
||||||
return options.storageState;
|
return options.storageState as any;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(await platform.fs().promises.readFile(options.storageState, 'utf8'));
|
return JSON.parse(await platform.fs().promises.readFile(options.storageState, 'utf8'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -338,16 +338,18 @@ export class APIResponse implements api.APIResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
async body(): Promise<Buffer> {
|
async body(): Promise<Buffer> {
|
||||||
try {
|
return await this._request._wrapApiCall(async () => {
|
||||||
const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() });
|
try {
|
||||||
if (result.binary === undefined)
|
const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() });
|
||||||
throw new Error('Response has been disposed');
|
if (result.binary === undefined)
|
||||||
return result.binary;
|
throw new Error('Response has been disposed');
|
||||||
} catch (e) {
|
return result.binary;
|
||||||
if (isTargetClosedError(e))
|
} catch (e) {
|
||||||
throw new Error('Response has been disposed');
|
if (isTargetClosedError(e))
|
||||||
throw e;
|
throw new Error('Response has been disposed');
|
||||||
}
|
throw e;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async text(): Promise<string> {
|
async text(): Promise<string> {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export type LocatorOptions = {
|
||||||
hasNotText?: string | RegExp;
|
hasNotText?: string | RegExp;
|
||||||
has?: Locator;
|
has?: Locator;
|
||||||
hasNot?: Locator;
|
hasNot?: Locator;
|
||||||
|
visible?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Locator implements api.Locator {
|
export class Locator implements api.Locator {
|
||||||
|
|
@ -65,6 +66,9 @@ export class Locator implements api.Locator {
|
||||||
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
|
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.visible !== undefined)
|
||||||
|
this._selector += ` >> visible=${options.visible ? 'true' : 'false'}`;
|
||||||
|
|
||||||
if (this._frame._platform.inspectCustom)
|
if (this._frame._platform.inspectCustom)
|
||||||
(this as any)[this._frame._platform.inspectCustom] = () => this._inspect();
|
(this as any)[this._frame._platform.inspectCustom] = () => this._inspect();
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +154,7 @@ export class Locator implements api.Locator {
|
||||||
return await this._frame._highlight(this._selector);
|
return await this._frame._highlight(this._selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator {
|
locator(selectorOrLocator: string | Locator, options?: Omit<LocatorOptions, 'visible'>): Locator {
|
||||||
if (isString(selectorOrLocator))
|
if (isString(selectorOrLocator))
|
||||||
return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options);
|
return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options);
|
||||||
if (selectorOrLocator._frame !== this._frame)
|
if (selectorOrLocator._frame !== this._frame)
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,11 @@ export type SelectOptionOptions = { force?: boolean, timeout?: number };
|
||||||
export type FilePayload = { name: string, mimeType: string, buffer: Buffer };
|
export type FilePayload = { name: string, mimeType: string, buffer: Buffer };
|
||||||
export type StorageState = {
|
export type StorageState = {
|
||||||
cookies: channels.NetworkCookie[],
|
cookies: channels.NetworkCookie[],
|
||||||
origins: channels.OriginStorage[],
|
origins: (Omit<channels.OriginStorage, 'indexedDB'> & { indexedDB: unknown[] })[],
|
||||||
};
|
};
|
||||||
export type SetStorageState = {
|
export type SetStorageState = {
|
||||||
cookies?: channels.SetNetworkCookie[],
|
cookies?: channels.SetNetworkCookie[],
|
||||||
origins?: channels.SetOriginStorage[]
|
origins?: (Omit<channels.SetOriginStorage, 'indexedDB'> & { indexedDB?: unknown[] })[]
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LifecycleEvent = channels.LifecycleEvent;
|
export type LifecycleEvent = channels.LifecycleEvent;
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,7 @@ scheme.PlaywrightNewRequestParams = tObject({
|
||||||
userAgent: tOptional(tString),
|
userAgent: tOptional(tString),
|
||||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
failOnStatusCode: tOptional(tBoolean),
|
||||||
clientCertificates: tOptional(tArray(tObject({
|
clientCertificates: tOptional(tArray(tObject({
|
||||||
origin: tString,
|
origin: tString,
|
||||||
cert: tOptional(tBinary),
|
cert: tOptional(tBinary),
|
||||||
|
|
@ -600,7 +600,6 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
|
||||||
})),
|
})),
|
||||||
permissions: tOptional(tArray(tString)),
|
permissions: tOptional(tArray(tString)),
|
||||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
|
||||||
offline: tOptional(tBoolean),
|
offline: tOptional(tBoolean),
|
||||||
httpCredentials: tOptional(tObject({
|
httpCredentials: tOptional(tObject({
|
||||||
username: tString,
|
username: tString,
|
||||||
|
|
@ -688,7 +687,6 @@ scheme.BrowserNewContextParams = tObject({
|
||||||
})),
|
})),
|
||||||
permissions: tOptional(tArray(tString)),
|
permissions: tOptional(tArray(tString)),
|
||||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
|
||||||
offline: tOptional(tBoolean),
|
offline: tOptional(tBoolean),
|
||||||
httpCredentials: tOptional(tObject({
|
httpCredentials: tOptional(tObject({
|
||||||
username: tString,
|
username: tString,
|
||||||
|
|
@ -759,7 +757,6 @@ scheme.BrowserNewContextForReuseParams = tObject({
|
||||||
})),
|
})),
|
||||||
permissions: tOptional(tArray(tString)),
|
permissions: tOptional(tArray(tString)),
|
||||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
|
||||||
offline: tOptional(tBoolean),
|
offline: tOptional(tBoolean),
|
||||||
httpCredentials: tOptional(tObject({
|
httpCredentials: tOptional(tObject({
|
||||||
username: tString,
|
username: tString,
|
||||||
|
|
@ -2667,7 +2664,6 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
|
||||||
})),
|
})),
|
||||||
permissions: tOptional(tArray(tString)),
|
permissions: tOptional(tArray(tString)),
|
||||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
|
||||||
offline: tOptional(tBoolean),
|
offline: tOptional(tBoolean),
|
||||||
httpCredentials: tOptional(tObject({
|
httpCredentials: tOptional(tObject({
|
||||||
username: tString,
|
username: tString,
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
||||||
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
|
async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
|
||||||
const response = await this._session.send('script.evaluate', {
|
const response = await this._session.send('script.evaluate', {
|
||||||
expression,
|
expression,
|
||||||
target: this._target,
|
target: this._target,
|
||||||
|
|
@ -72,7 +72,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
||||||
});
|
});
|
||||||
if (response.type === 'success') {
|
if (response.type === 'success') {
|
||||||
if ('handle' in response.result)
|
if ('handle' in response.result)
|
||||||
return response.result.handle!;
|
return createHandle(context, response.result);
|
||||||
throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result));
|
throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result));
|
||||||
}
|
}
|
||||||
if (response.type === 'exception')
|
if (response.type === 'exception')
|
||||||
|
|
@ -80,14 +80,14 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
||||||
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], handles: js.JSHandle[]): Promise<any> {
|
||||||
const response = await this._session.send('script.callFunction', {
|
const response = await this._session.send('script.callFunction', {
|
||||||
functionDeclaration,
|
functionDeclaration,
|
||||||
target: this._target,
|
target: this._target,
|
||||||
arguments: [
|
arguments: [
|
||||||
{ handle: utilityScript._objectId! },
|
{ handle: utilityScript._objectId! },
|
||||||
...values.map(BidiSerializer.serialize),
|
...values.map(BidiSerializer.serialize),
|
||||||
...objectIds.map(handle => ({ handle })),
|
...handles.map(handle => ({ handle: handle._objectId! })),
|
||||||
],
|
],
|
||||||
resultOwnership: returnByValue ? undefined : bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
|
resultOwnership: returnByValue ? undefined : bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
|
||||||
serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 },
|
serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 },
|
||||||
|
|
@ -121,10 +121,12 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
async releaseHandle(objectId: js.ObjectId): Promise<void> {
|
async releaseHandle(handle: js.JSHandle): Promise<void> {
|
||||||
|
if (!handle._objectId)
|
||||||
|
return;
|
||||||
await this._session.send('script.disown', {
|
await this._session.send('script.disown', {
|
||||||
target: this._target,
|
target: this._target,
|
||||||
handles: [objectId],
|
handles: [handle._objectId],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,6 @@ export class RawMouseImpl implements input.RawMouse {
|
||||||
}
|
}
|
||||||
|
|
||||||
async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void> {
|
async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void> {
|
||||||
// Bidi throws when x/y are not integers.
|
|
||||||
x = Math.floor(x);
|
|
||||||
y = Math.floor(y);
|
|
||||||
await this._performActions([{ type: 'pointerMove', x, y }]);
|
await this._performActions([{ type: 'pointerMove', x, y }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -511,7 +511,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
this._origins.add(origin);
|
this._origins.add(origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
async storageState(indexedDB = true): Promise<channels.BrowserContextStorageStateResult> {
|
async storageState(indexedDB = false): Promise<channels.BrowserContextStorageStateResult> {
|
||||||
const result: channels.BrowserContextStorageStateResult = {
|
const result: channels.BrowserContextStorageStateResult = {
|
||||||
cookies: await this.cookies(),
|
cookies: await this.cookies(),
|
||||||
origins: []
|
origins: []
|
||||||
|
|
|
||||||
|
|
@ -46,24 +46,24 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return remoteObject.value;
|
return remoteObject.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
|
async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
|
||||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
|
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
|
||||||
expression,
|
expression,
|
||||||
contextId: this._contextId,
|
contextId: this._contextId,
|
||||||
}).catch(rewriteError);
|
}).catch(rewriteError);
|
||||||
if (exceptionDetails)
|
if (exceptionDetails)
|
||||||
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
|
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
|
||||||
return remoteObject.objectId!;
|
return createHandle(context, remoteObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], handles: js.JSHandle[]): Promise<any> {
|
||||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: expression,
|
functionDeclaration: expression,
|
||||||
objectId: utilityScript._objectId,
|
objectId: utilityScript._objectId,
|
||||||
arguments: [
|
arguments: [
|
||||||
{ objectId: utilityScript._objectId },
|
{ objectId: utilityScript._objectId },
|
||||||
...values.map(value => ({ value })),
|
...values.map(value => ({ value })),
|
||||||
...objectIds.map(objectId => ({ objectId })),
|
...handles.map(handle => ({ objectId: handle._objectId! })),
|
||||||
],
|
],
|
||||||
returnByValue,
|
returnByValue,
|
||||||
awaitPromise: true,
|
awaitPromise: true,
|
||||||
|
|
@ -88,8 +88,10 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async releaseHandle(objectId: js.ObjectId): Promise<void> {
|
async releaseHandle(handle: js.JSHandle): Promise<void> {
|
||||||
await releaseObject(this._client, objectId);
|
if (!handle._objectId)
|
||||||
|
return;
|
||||||
|
await releaseObject(this._client, handle._objectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Galaxy S5": {
|
"Galaxy S5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S5 landscape": {
|
"Galaxy S5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -132,7 +132,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8": {
|
"Galaxy S8": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 740
|
"height": 740
|
||||||
|
|
@ -143,7 +143,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8 landscape": {
|
"Galaxy S8 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 740,
|
"width": 740,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+": {
|
"Galaxy S9+": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 320,
|
"width": 320,
|
||||||
"height": 658
|
"height": 658
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+ landscape": {
|
"Galaxy S9+ landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 658,
|
"width": 658,
|
||||||
"height": 320
|
"height": 320
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4": {
|
"Galaxy Tab S4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 712,
|
"width": 712,
|
||||||
"height": 1138
|
"height": 1138
|
||||||
|
|
@ -187,7 +187,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4 landscape": {
|
"Galaxy Tab S4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 1138,
|
"width": 1138,
|
||||||
"height": 712
|
"height": 712
|
||||||
|
|
@ -1098,7 +1098,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"LG Optimus L70": {
|
"LG Optimus L70": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1109,7 +1109,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"LG Optimus L70 landscape": {
|
"LG Optimus L70 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1120,7 +1120,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550": {
|
"Microsoft Lumia 550": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1131,7 +1131,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550 landscape": {
|
"Microsoft Lumia 550 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1142,7 +1142,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950": {
|
"Microsoft Lumia 950": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1153,7 +1153,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950 landscape": {
|
"Microsoft Lumia 950 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1164,7 +1164,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10": {
|
"Nexus 10": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 1280
|
"height": 1280
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10 landscape": {
|
"Nexus 10 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 800
|
"height": 800
|
||||||
|
|
@ -1186,7 +1186,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4": {
|
"Nexus 4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1197,7 +1197,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4 landscape": {
|
"Nexus 4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1208,7 +1208,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5": {
|
"Nexus 5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1219,7 +1219,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5 landscape": {
|
"Nexus 5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1230,7 +1230,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X": {
|
"Nexus 5X": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1241,7 +1241,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X landscape": {
|
"Nexus 5X landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1252,7 +1252,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6": {
|
"Nexus 6": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1263,7 +1263,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6 landscape": {
|
"Nexus 6 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1274,7 +1274,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P": {
|
"Nexus 6P": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1285,7 +1285,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P landscape": {
|
"Nexus 6P landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1296,7 +1296,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7": {
|
"Nexus 7": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 960
|
"height": 960
|
||||||
|
|
@ -1307,7 +1307,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7 landscape": {
|
"Nexus 7 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 960,
|
"width": 960,
|
||||||
"height": 600
|
"height": 600
|
||||||
|
|
@ -1362,7 +1362,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Pixel 2": {
|
"Pixel 2": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 731
|
"height": 731
|
||||||
|
|
@ -1373,7 +1373,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 landscape": {
|
"Pixel 2 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 731,
|
"width": 731,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1384,7 +1384,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL": {
|
"Pixel 2 XL": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 823
|
"height": 823
|
||||||
|
|
@ -1395,7 +1395,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL landscape": {
|
"Pixel 2 XL landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 823,
|
"width": 823,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1406,7 +1406,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3": {
|
"Pixel 3": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 786
|
"height": 786
|
||||||
|
|
@ -1417,7 +1417,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3 landscape": {
|
"Pixel 3 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 786,
|
"width": 786,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1428,7 +1428,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4": {
|
"Pixel 4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 353,
|
"width": 353,
|
||||||
"height": 745
|
"height": 745
|
||||||
|
|
@ -1439,7 +1439,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4 landscape": {
|
"Pixel 4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 745,
|
"width": 745,
|
||||||
"height": 353
|
"height": 353
|
||||||
|
|
@ -1450,7 +1450,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G)": {
|
"Pixel 4a (5G)": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 892
|
"height": 892
|
||||||
|
|
@ -1465,7 +1465,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G) landscape": {
|
"Pixel 4a (5G) landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"height": 892,
|
"height": 892,
|
||||||
"width": 412
|
"width": 412
|
||||||
|
|
@ -1480,7 +1480,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5": {
|
"Pixel 5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 851
|
"height": 851
|
||||||
|
|
@ -1495,7 +1495,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5 landscape": {
|
"Pixel 5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 851,
|
"width": 851,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1510,7 +1510,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7": {
|
"Pixel 7": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 915
|
"height": 915
|
||||||
|
|
@ -1525,7 +1525,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7 landscape": {
|
"Pixel 7 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 915,
|
"width": 915,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1540,7 +1540,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4": {
|
"Moto G4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1551,7 +1551,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4 landscape": {
|
"Moto G4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1562,7 +1562,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Chrome HiDPI": {
|
"Desktop Chrome HiDPI": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1577,7 +1577,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge HiDPI": {
|
"Desktop Edge HiDPI": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36 Edg/134.0.6998.23",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36 Edg/134.0.6998.35",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1622,7 +1622,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Desktop Chrome": {
|
"Desktop Chrome": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
@ -1637,7 +1637,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge": {
|
"Desktop Edge": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36 Edg/134.0.6998.23",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.35 Safari/537.36 Edg/134.0.6998.35",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,11 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
`;
|
`;
|
||||||
this._injectedScriptPromise = this.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', 'InjectedScript', objectId));
|
this._injectedScriptPromise = this.rawEvaluateHandle(source)
|
||||||
|
.then(handle => {
|
||||||
|
handle._setPreview('InjectedScript');
|
||||||
|
return handle;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return this._injectedScriptPromise;
|
return this._injectedScriptPromise;
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +122,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
declare readonly _objectId: string;
|
declare readonly _objectId: string;
|
||||||
readonly _frame: frames.Frame;
|
readonly _frame: frames.Frame;
|
||||||
|
|
||||||
constructor(context: FrameExecutionContext, objectId: js.ObjectId) {
|
constructor(context: FrameExecutionContext, objectId: string) {
|
||||||
super(context, 'node', undefined, objectId);
|
super(context, 'node', undefined, objectId);
|
||||||
this._page = context.frame._page;
|
this._page = context.frame._page;
|
||||||
this._frame = context.frame;
|
this._frame = context.frame;
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ import type { Readable, TransformCallback } from 'stream';
|
||||||
type FetchRequestOptions = {
|
type FetchRequestOptions = {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
extraHTTPHeaders?: HeadersArray;
|
extraHTTPHeaders?: HeadersArray;
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
failOnStatusCode?: boolean;
|
||||||
httpCredentials?: HTTPCredentials;
|
httpCredentials?: HTTPCredentials;
|
||||||
proxy?: ProxySettings;
|
proxy?: ProxySettings;
|
||||||
timeoutSettings: TimeoutSettings;
|
timeoutSettings: TimeoutSettings;
|
||||||
|
|
@ -212,7 +212,7 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
});
|
});
|
||||||
const fetchUid = this._storeResponseBody(fetchResponse.body);
|
const fetchUid = this._storeResponseBody(fetchResponse.body);
|
||||||
this.fetchLog.set(fetchUid, controller.metadata.log);
|
this.fetchLog.set(fetchUid, controller.metadata.log);
|
||||||
const failOnStatusCode = params.failOnStatusCode !== undefined ? params.failOnStatusCode : !!defaults.apiRequestFailsOnErrorStatus;
|
const failOnStatusCode = params.failOnStatusCode !== undefined ? params.failOnStatusCode : !!defaults.failOnStatusCode;
|
||||||
if (failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400)) {
|
if (failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400)) {
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
if (fetchResponse.body.byteLength) {
|
if (fetchResponse.body.byteLength) {
|
||||||
|
|
@ -608,7 +608,7 @@ export class BrowserContextAPIRequestContext extends APIRequestContext {
|
||||||
return {
|
return {
|
||||||
userAgent: this._context._options.userAgent || this._context._browser.userAgent(),
|
userAgent: this._context._options.userAgent || this._context._browser.userAgent(),
|
||||||
extraHTTPHeaders: this._context._options.extraHTTPHeaders,
|
extraHTTPHeaders: this._context._options.extraHTTPHeaders,
|
||||||
apiRequestFailsOnErrorStatus: this._context._options.apiRequestFailsOnErrorStatus,
|
failOnStatusCode: undefined,
|
||||||
httpCredentials: this._context._options.httpCredentials,
|
httpCredentials: this._context._options.httpCredentials,
|
||||||
proxy: this._context._options.proxy || this._context._browser.options.proxy,
|
proxy: this._context._options.proxy || this._context._browser.options.proxy,
|
||||||
timeoutSettings: this._context._timeoutSettings,
|
timeoutSettings: this._context._timeoutSettings,
|
||||||
|
|
@ -660,7 +660,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
||||||
baseURL: options.baseURL,
|
baseURL: options.baseURL,
|
||||||
userAgent: options.userAgent || getUserAgent(),
|
userAgent: options.userAgent || getUserAgent(),
|
||||||
extraHTTPHeaders: options.extraHTTPHeaders,
|
extraHTTPHeaders: options.extraHTTPHeaders,
|
||||||
apiRequestFailsOnErrorStatus: !!options.apiRequestFailsOnErrorStatus,
|
failOnStatusCode: !!options.failOnStatusCode,
|
||||||
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
|
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
|
||||||
httpCredentials: options.httpCredentials,
|
httpCredentials: options.httpCredentials,
|
||||||
clientCertificates: options.clientCertificates,
|
clientCertificates: options.clientCertificates,
|
||||||
|
|
@ -693,7 +693,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
||||||
return this._cookieStore.cookies(url);
|
return this._cookieStore.cookies(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async storageState(indexedDB = true): Promise<channels.APIRequestContextStorageStateResult> {
|
override async storageState(indexedDB = false): Promise<channels.APIRequestContextStorageStateResult> {
|
||||||
return {
|
return {
|
||||||
cookies: this._cookieStore.allCookies(),
|
cookies: this._cookieStore.allCookies(),
|
||||||
origins: (this._origins || []).map(origin => ({ ...origin, indexedDB: indexedDB ? origin.indexedDB : [] })),
|
origins: (this._origins || []).map(origin => ({ ...origin, indexedDB: indexedDB ? origin.indexedDB : [] })),
|
||||||
|
|
|
||||||
|
|
@ -44,23 +44,23 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return payload.result!.value;
|
return payload.result!.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
|
async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
|
||||||
const payload = await this._session.send('Runtime.evaluate', {
|
const payload = await this._session.send('Runtime.evaluate', {
|
||||||
expression,
|
expression,
|
||||||
returnByValue: false,
|
returnByValue: false,
|
||||||
executionContextId: this._executionContextId,
|
executionContextId: this._executionContextId,
|
||||||
}).catch(rewriteError);
|
}).catch(rewriteError);
|
||||||
checkException(payload.exceptionDetails);
|
checkException(payload.exceptionDetails);
|
||||||
return payload.result!.objectId!;
|
return createHandle(context, payload.result!);
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], handles: js.JSHandle[]): Promise<any> {
|
||||||
const payload = await this._session.send('Runtime.callFunction', {
|
const payload = await this._session.send('Runtime.callFunction', {
|
||||||
functionDeclaration: expression,
|
functionDeclaration: expression,
|
||||||
args: [
|
args: [
|
||||||
{ objectId: utilityScript._objectId, value: undefined },
|
{ objectId: utilityScript._objectId, value: undefined },
|
||||||
...values.map(value => ({ value })),
|
...values.map(value => ({ value })),
|
||||||
...objectIds.map(objectId => ({ objectId, value: undefined })),
|
...handles.map(handle => ({ objectId: handle._objectId!, value: undefined })),
|
||||||
],
|
],
|
||||||
returnByValue,
|
returnByValue,
|
||||||
executionContextId: this._executionContextId
|
executionContextId: this._executionContextId
|
||||||
|
|
@ -82,10 +82,12 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async releaseHandle(objectId: js.ObjectId): Promise<void> {
|
async releaseHandle(handle: js.JSHandle): Promise<void> {
|
||||||
|
if (!handle._objectId)
|
||||||
|
return;
|
||||||
await this._session.send('Runtime.disposeObject', {
|
await this._session.send('Runtime.disposeObject', {
|
||||||
executionContextId: this._executionContextId,
|
executionContextId: this._executionContextId,
|
||||||
objectId
|
objectId: handle._objectId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class Locator {
|
||||||
element: Element | undefined;
|
element: Element | undefined;
|
||||||
elements: Element[] | undefined;
|
elements: Element[] | undefined;
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator }) {
|
constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator, visible?: boolean }) {
|
||||||
if (options?.hasText)
|
if (options?.hasText)
|
||||||
selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
|
selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
|
||||||
if (options?.hasNotText)
|
if (options?.hasNotText)
|
||||||
|
|
@ -38,6 +38,8 @@ class Locator {
|
||||||
selector += ` >> internal:has=` + JSON.stringify(options.has[selectorSymbol]);
|
selector += ` >> internal:has=` + JSON.stringify(options.has[selectorSymbol]);
|
||||||
if (options?.hasNot)
|
if (options?.hasNot)
|
||||||
selector += ` >> internal:has-not=` + JSON.stringify(options.hasNot[selectorSymbol]);
|
selector += ` >> internal:has-not=` + JSON.stringify(options.hasNot[selectorSymbol]);
|
||||||
|
if (options?.visible !== undefined)
|
||||||
|
selector += ` >> visible=${options.visible ? 'true' : 'false'}`;
|
||||||
this[selectorSymbol] = selector;
|
this[selectorSymbol] = selector;
|
||||||
if (selector) {
|
if (selector) {
|
||||||
const parsed = injectedScript.parseSelector(selector);
|
const parsed = injectedScript.parseSelector(selector);
|
||||||
|
|
@ -46,7 +48,7 @@ class Locator {
|
||||||
}
|
}
|
||||||
const selectorBase = selector;
|
const selectorBase = selector;
|
||||||
const self = this as any;
|
const self = this as any;
|
||||||
self.locator = (selector: string, options?: { hasText?: string | RegExp, has?: Locator }): Locator => {
|
self.locator = (selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator }): Locator => {
|
||||||
return new Locator(injectedScript, selectorBase ? selectorBase + ' >> ' + selector : selector, options);
|
return new Locator(injectedScript, selectorBase ? selectorBase + ' >> ' + selector : selector, options);
|
||||||
};
|
};
|
||||||
self.getByTestId = (testId: string): Locator => self.locator(getByTestIdSelector(injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen(), testId));
|
self.getByTestId = (testId: string): Locator => self.locator(getByTestIdSelector(injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen(), testId));
|
||||||
|
|
@ -56,7 +58,7 @@ class Locator {
|
||||||
self.getByText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTextSelector(text, options));
|
self.getByText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTextSelector(text, options));
|
||||||
self.getByTitle = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTitleSelector(text, options));
|
self.getByTitle = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTitleSelector(text, options));
|
||||||
self.getByRole = (role: string, options: ByRoleOptions = {}): Locator => self.locator(getByRoleSelector(role, options));
|
self.getByRole = (role: string, options: ByRoleOptions = {}): Locator => self.locator(getByRoleSelector(role, options));
|
||||||
self.filter = (options?: { hasText?: string | RegExp, has?: Locator }): Locator => new Locator(injectedScript, selector, options);
|
self.filter = (options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator, visible?: boolean }): Locator => new Locator(injectedScript, selector, options);
|
||||||
self.first = (): Locator => self.locator('nth=0');
|
self.first = (): Locator => self.locator('nth=0');
|
||||||
self.last = (): Locator => self.locator('nth=-1');
|
self.last = (): Locator => self.locator('nth=-1');
|
||||||
self.nth = (index: number): Locator => self.locator(`nth=${index}`);
|
self.nth = (index: number): Locator => self.locator(`nth=${index}`);
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,7 @@ function trimFlatString(s: string): string {
|
||||||
function asFlatString(s: string): string {
|
function asFlatString(s: string): string {
|
||||||
// "Flat string" at https://w3c.github.io/accname/#terminology
|
// "Flat string" at https://w3c.github.io/accname/#terminology
|
||||||
// Note that non-breaking spaces are preserved.
|
// Note that non-breaking spaces are preserved.
|
||||||
return s.split('\u00A0').map(chunk => chunk.replace(/\r\n/g, '\n').replace(/\s\s*/g, ' ')).join('\u00A0').trim();
|
return s.split('\u00A0').map(chunk => chunk.replace(/\r\n/g, '\n').replace(/[\u200b\u00ad]/g, '').replace(/\s\s*/g, ' ')).join('\u00A0').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryInAriaOwned(element: Element, selector: string): Element[] {
|
function queryInAriaOwned(element: Element, selector: string): Element[] {
|
||||||
|
|
|
||||||
|
|
@ -129,10 +129,13 @@ export function source() {
|
||||||
|
|
||||||
function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
|
function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
|
||||||
if (value && typeof value === 'object') {
|
if (value && typeof value === 'object') {
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
if (typeof globalThis.Window === 'function' && value instanceof globalThis.Window)
|
if (typeof globalThis.Window === 'function' && value instanceof globalThis.Window)
|
||||||
return 'ref: <Window>';
|
return 'ref: <Window>';
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
if (typeof globalThis.Document === 'function' && value instanceof globalThis.Document)
|
if (typeof globalThis.Document === 'function' && value instanceof globalThis.Document)
|
||||||
return 'ref: <Document>';
|
return 'ref: <Document>';
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
if (typeof globalThis.Node === 'function' && value instanceof globalThis.Node)
|
if (typeof globalThis.Node === 'function' && value instanceof globalThis.Node)
|
||||||
return 'ref: <Node>';
|
return 'ref: <Node>';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ import { LongStandingScope } from '../utils/isomorphic/manualPromise';
|
||||||
import type * as dom from './dom';
|
import type * as dom from './dom';
|
||||||
import type { UtilityScript } from './injected/utilityScript';
|
import type { UtilityScript } from './injected/utilityScript';
|
||||||
|
|
||||||
export type ObjectId = string;
|
|
||||||
|
|
||||||
interface TaggedAsJSHandle<T> {
|
interface TaggedAsJSHandle<T> {
|
||||||
__jshandle: T;
|
__jshandle: T;
|
||||||
}
|
}
|
||||||
|
|
@ -49,10 +47,10 @@ export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>
|
||||||
|
|
||||||
export interface ExecutionContextDelegate {
|
export interface ExecutionContextDelegate {
|
||||||
rawEvaluateJSON(expression: string): Promise<any>;
|
rawEvaluateJSON(expression: string): Promise<any>;
|
||||||
rawEvaluateHandle(expression: string): Promise<ObjectId>;
|
rawEvaluateHandle(context: ExecutionContext, expression: string): Promise<JSHandle>;
|
||||||
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>;
|
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle, values: any[], handles: JSHandle[]): Promise<any>;
|
||||||
getProperties(object: JSHandle): Promise<Map<string, JSHandle>>;
|
getProperties(object: JSHandle): Promise<Map<string, JSHandle>>;
|
||||||
releaseHandle(objectId: ObjectId): Promise<void>;
|
releaseHandle(handle: JSHandle): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExecutionContext extends SdkObject {
|
export class ExecutionContext extends SdkObject {
|
||||||
|
|
@ -79,21 +77,21 @@ export class ExecutionContext extends SdkObject {
|
||||||
return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateJSON(expression));
|
return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateJSON(expression));
|
||||||
}
|
}
|
||||||
|
|
||||||
rawEvaluateHandle(expression: string): Promise<ObjectId> {
|
rawEvaluateHandle(expression: string): Promise<JSHandle> {
|
||||||
return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(expression));
|
return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, expression));
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateWithArguments(expression: string, returnByValue: boolean, values: any[], objectIds: ObjectId[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, values: any[], handles: JSHandle[]): Promise<any> {
|
||||||
const utilityScript = await this._utilityScript();
|
const utilityScript = await this._utilityScript();
|
||||||
return this._raceAgainstContextDestroyed(this.delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds));
|
return this._raceAgainstContextDestroyed(this.delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, handles));
|
||||||
}
|
}
|
||||||
|
|
||||||
getProperties(object: JSHandle): Promise<Map<string, JSHandle>> {
|
getProperties(object: JSHandle): Promise<Map<string, JSHandle>> {
|
||||||
return this._raceAgainstContextDestroyed(this.delegate.getProperties(object));
|
return this._raceAgainstContextDestroyed(this.delegate.getProperties(object));
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseHandle(objectId: ObjectId): Promise<void> {
|
releaseHandle(handle: JSHandle): Promise<void> {
|
||||||
return this.delegate.releaseHandle(objectId);
|
return this.delegate.releaseHandle(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
adoptIfNeeded(handle: JSHandle): Promise<JSHandle> | null {
|
adoptIfNeeded(handle: JSHandle): Promise<JSHandle> | null {
|
||||||
|
|
@ -108,7 +106,11 @@ export class ExecutionContext extends SdkObject {
|
||||||
${utilityScriptSource.source}
|
${utilityScriptSource.source}
|
||||||
return new (module.exports.UtilityScript())(${isUnderTest()});
|
return new (module.exports.UtilityScript())(${isUnderTest()});
|
||||||
})();`;
|
})();`;
|
||||||
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', 'UtilityScript', objectId)));
|
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, source))
|
||||||
|
.then(handle => {
|
||||||
|
handle._setPreview('UtilityScript');
|
||||||
|
return handle;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return this._utilityScriptPromise;
|
return this._utilityScriptPromise;
|
||||||
}
|
}
|
||||||
|
|
@ -122,13 +124,13 @@ export class JSHandle<T = any> extends SdkObject {
|
||||||
__jshandle: T = true as any;
|
__jshandle: T = true as any;
|
||||||
readonly _context: ExecutionContext;
|
readonly _context: ExecutionContext;
|
||||||
_disposed = false;
|
_disposed = false;
|
||||||
readonly _objectId: ObjectId | undefined;
|
readonly _objectId: string | undefined;
|
||||||
readonly _value: any;
|
readonly _value: any;
|
||||||
private _objectType: string;
|
private _objectType: string;
|
||||||
protected _preview: string;
|
protected _preview: string;
|
||||||
private _previewCallback: ((preview: string) => void) | undefined;
|
private _previewCallback: ((preview: string) => void) | undefined;
|
||||||
|
|
||||||
constructor(context: ExecutionContext, type: string, preview: string | undefined, objectId?: ObjectId, value?: any) {
|
constructor(context: ExecutionContext, type: string, preview: string | undefined, objectId?: string, value?: any) {
|
||||||
super(context, 'handle');
|
super(context, 'handle');
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._objectId = objectId;
|
this._objectId = objectId;
|
||||||
|
|
@ -185,7 +187,7 @@ export class JSHandle<T = any> extends SdkObject {
|
||||||
if (!this._objectId)
|
if (!this._objectId)
|
||||||
return this._value;
|
return this._value;
|
||||||
const script = `(utilityScript, ...args) => utilityScript.jsonValue(...args)`;
|
const script = `(utilityScript, ...args) => utilityScript.jsonValue(...args)`;
|
||||||
return this._context.evaluateWithArguments(script, true, [true], [this._objectId]);
|
return this._context.evaluateWithArguments(script, true, [true], [this]);
|
||||||
}
|
}
|
||||||
|
|
||||||
asElement(): dom.ElementHandle | null {
|
asElement(): dom.ElementHandle | null {
|
||||||
|
|
@ -197,7 +199,7 @@ export class JSHandle<T = any> extends SdkObject {
|
||||||
return;
|
return;
|
||||||
this._disposed = true;
|
this._disposed = true;
|
||||||
if (this._objectId) {
|
if (this._objectId) {
|
||||||
this._context.releaseHandle(this._objectId).catch(e => {});
|
this._context.releaseHandle(this).catch(e => {});
|
||||||
if ((globalThis as any).leakedJSHandles)
|
if ((globalThis as any).leakedJSHandles)
|
||||||
(globalThis as any).leakedJSHandles.delete(this);
|
(globalThis as any).leakedJSHandles.delete(this);
|
||||||
}
|
}
|
||||||
|
|
@ -254,11 +256,11 @@ export async function evaluateExpression(context: ExecutionContext, expression:
|
||||||
return { fallThrough: handle };
|
return { fallThrough: handle };
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const utilityScriptObjectIds: ObjectId[] = [];
|
const utilityScriptObjects: JSHandle[] = [];
|
||||||
for (const handle of await Promise.all(handles)) {
|
for (const handle of await Promise.all(handles)) {
|
||||||
if (handle._context !== context)
|
if (handle._context !== context)
|
||||||
throw new JavaScriptErrorInEvaluate('JSHandles can be evaluated only in the context they were created!');
|
throw new JavaScriptErrorInEvaluate('JSHandles can be evaluated only in the context they were created!');
|
||||||
utilityScriptObjectIds.push(handle._objectId!);
|
utilityScriptObjects.push(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// See UtilityScript for arguments.
|
// See UtilityScript for arguments.
|
||||||
|
|
@ -266,7 +268,7 @@ export async function evaluateExpression(context: ExecutionContext, expression:
|
||||||
|
|
||||||
const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`;
|
const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`;
|
||||||
try {
|
try {
|
||||||
return await context.evaluateWithArguments(script, options.returnByValue || false, utilityScriptValues, utilityScriptObjectIds);
|
return await context.evaluateWithArguments(script, options.returnByValue || false, utilityScriptValues, utilityScriptObjects);
|
||||||
} finally {
|
} finally {
|
||||||
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
|
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
|
async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
|
||||||
try {
|
try {
|
||||||
const response = await this._session.send('Runtime.evaluate', {
|
const response = await this._session.send('Runtime.evaluate', {
|
||||||
expression,
|
expression,
|
||||||
|
|
@ -57,13 +57,13 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
});
|
});
|
||||||
if (response.wasThrown)
|
if (response.wasThrown)
|
||||||
throw new js.JavaScriptErrorInEvaluate(response.result.description);
|
throw new js.JavaScriptErrorInEvaluate(response.result.description);
|
||||||
return response.result.objectId!;
|
return createHandle(context, response.result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw rewriteError(error);
|
throw rewriteError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], handles: js.JSHandle[]): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await this._session.send('Runtime.callFunctionOn', {
|
const response = await this._session.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: expression,
|
functionDeclaration: expression,
|
||||||
|
|
@ -71,7 +71,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
arguments: [
|
arguments: [
|
||||||
{ objectId: utilityScript._objectId },
|
{ objectId: utilityScript._objectId },
|
||||||
...values.map(value => ({ value })),
|
...values.map(value => ({ value })),
|
||||||
...objectIds.map(objectId => ({ objectId })),
|
...handles.map(handle => ({ objectId: handle._objectId! })),
|
||||||
],
|
],
|
||||||
returnByValue,
|
returnByValue,
|
||||||
emulateUserGesture: true,
|
emulateUserGesture: true,
|
||||||
|
|
@ -101,8 +101,10 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async releaseHandle(objectId: js.ObjectId): Promise<void> {
|
async releaseHandle(handle: js.JSHandle): Promise<void> {
|
||||||
await this._session.send('Runtime.releaseObject', { objectId });
|
if (!handle._objectId)
|
||||||
|
return;
|
||||||
|
await this._session.send('Runtime.releaseObject', { objectId: handle._objectId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,8 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: yaml
|
||||||
const emptyFragment: AriaTemplateRoleNode = { kind: 'role', role: 'fragment' };
|
const emptyFragment: AriaTemplateRoleNode = { kind: 'role', role: 'fragment' };
|
||||||
|
|
||||||
function normalizeWhitespace(text: string) {
|
function normalizeWhitespace(text: string) {
|
||||||
return text.replace(/[\r\n\s\t]+/g, ' ').trim();
|
// TODO: why is this different from normalizeWhitespace in stringUtils.ts?
|
||||||
|
return text.replace(/[\u200b\u00ad]/g, '').replace(/[\r\n\s\t]+/g, ' ').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function valueOrRegex(value: string): string | AriaRegex {
|
export function valueOrRegex(value: string): string | AriaRegex {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import type { NestedSelectorBody } from './selectorParser';
|
||||||
import type { ParsedSelector } from './selectorParser';
|
import type { ParsedSelector } from './selectorParser';
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
||||||
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'frame-locator' | 'and' | 'or' | 'chain';
|
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'visible' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'frame-locator' | 'and' | 'or' | 'chain';
|
||||||
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||||
export type Quote = '\'' | '"' | '`';
|
export type Quote = '\'' | '"' | '`';
|
||||||
|
|
||||||
|
|
@ -68,6 +68,10 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
|
||||||
tokens.push([factory.generateLocator(base, 'nth', part.body as string)]);
|
tokens.push([factory.generateLocator(base, 'nth', part.body as string)]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (part.name === 'visible') {
|
||||||
|
tokens.push([factory.generateLocator(base, 'visible', part.body as string), factory.generateLocator(base, 'default', `visible=${part.body}`)]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (part.name === 'internal:text') {
|
if (part.name === 'internal:text') {
|
||||||
const { exact, text } = detectExact(part.body as string);
|
const { exact, text } = detectExact(part.body as string);
|
||||||
tokens.push([factory.generateLocator(base, 'text', text, { exact })]);
|
tokens.push([factory.generateLocator(base, 'text', text, { exact })]);
|
||||||
|
|
@ -275,6 +279,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||||
return `first()`;
|
return `first()`;
|
||||||
case 'last':
|
case 'last':
|
||||||
return `last()`;
|
return `last()`;
|
||||||
|
case 'visible':
|
||||||
|
return `filter({ visible: ${body === 'true' ? 'true' : 'false'} })`;
|
||||||
case 'role':
|
case 'role':
|
||||||
const attrs: string[] = [];
|
const attrs: string[] = [];
|
||||||
if (isRegExp(options.name)) {
|
if (isRegExp(options.name)) {
|
||||||
|
|
@ -369,6 +375,8 @@ export class PythonLocatorFactory implements LocatorFactory {
|
||||||
return `first`;
|
return `first`;
|
||||||
case 'last':
|
case 'last':
|
||||||
return `last`;
|
return `last`;
|
||||||
|
case 'visible':
|
||||||
|
return `filter(visible=${body === 'true' ? 'True' : 'False'})`;
|
||||||
case 'role':
|
case 'role':
|
||||||
const attrs: string[] = [];
|
const attrs: string[] = [];
|
||||||
if (isRegExp(options.name)) {
|
if (isRegExp(options.name)) {
|
||||||
|
|
@ -476,6 +484,8 @@ export class JavaLocatorFactory implements LocatorFactory {
|
||||||
return `first()`;
|
return `first()`;
|
||||||
case 'last':
|
case 'last':
|
||||||
return `last()`;
|
return `last()`;
|
||||||
|
case 'visible':
|
||||||
|
return `filter(new ${clazz}.FilterOptions().setVisible(${body === 'true' ? 'true' : 'false'}))`;
|
||||||
case 'role':
|
case 'role':
|
||||||
const attrs: string[] = [];
|
const attrs: string[] = [];
|
||||||
if (isRegExp(options.name)) {
|
if (isRegExp(options.name)) {
|
||||||
|
|
@ -573,6 +583,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||||
return `First`;
|
return `First`;
|
||||||
case 'last':
|
case 'last':
|
||||||
return `Last`;
|
return `Last`;
|
||||||
|
case 'visible':
|
||||||
|
return `Filter(new() { Visible = ${body === 'true' ? 'true' : 'false'} })`;
|
||||||
case 'role':
|
case 'role':
|
||||||
const attrs: string[] = [];
|
const attrs: string[] = [];
|
||||||
if (isRegExp(options.name)) {
|
if (isRegExp(options.name)) {
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,8 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
||||||
.replace(/first(\(\))?/g, 'nth=0')
|
.replace(/first(\(\))?/g, 'nth=0')
|
||||||
.replace(/last(\(\))?/g, 'nth=-1')
|
.replace(/last(\(\))?/g, 'nth=-1')
|
||||||
.replace(/nth\(([^)]+)\)/g, 'nth=$1')
|
.replace(/nth\(([^)]+)\)/g, 'nth=$1')
|
||||||
|
.replace(/filter\(,?visible=true\)/g, 'visible=true')
|
||||||
|
.replace(/filter\(,?visible=false\)/g, 'visible=false')
|
||||||
.replace(/filter\(,?hastext=([^)]+)\)/g, 'internal:has-text=$1')
|
.replace(/filter\(,?hastext=([^)]+)\)/g, 'internal:has-text=$1')
|
||||||
.replace(/filter\(,?hasnottext=([^)]+)\)/g, 'internal:has-not-text=$1')
|
.replace(/filter\(,?hasnottext=([^)]+)\)/g, 'internal:has-not-text=$1')
|
||||||
.replace(/filter\(,?has2=([^)]+)\)/g, 'internal:has=$1')
|
.replace(/filter\(,?has2=([^)]+)\)/g, 'internal:has=$1')
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ export function cacheNormalizedWhitespaces() {
|
||||||
export function normalizeWhiteSpace(text: string): string {
|
export function normalizeWhiteSpace(text: string): string {
|
||||||
let result = normalizedWhitespaceCache?.get(text);
|
let result = normalizedWhitespaceCache?.get(text);
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
result = text.replace(/\u200b/g, '').trim().replace(/\s+/g, ' ');
|
result = text.replace(/[\u200b\u00ad]/g, '').trim().replace(/\s+/g, ' ');
|
||||||
normalizedWhitespaceCache?.set(text, result);
|
normalizedWhitespaceCache?.set(text, result);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
296
packages/playwright-core/types/types.d.ts
vendored
296
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -9267,14 +9267,15 @@ export interface BrowserContext {
|
||||||
/**
|
/**
|
||||||
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB
|
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB
|
||||||
* snapshot.
|
* snapshot.
|
||||||
*
|
|
||||||
* **NOTE** IndexedDBs with typed arrays are currently not supported.
|
|
||||||
*
|
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
storageState(options?: {
|
storageState(options?: {
|
||||||
/**
|
/**
|
||||||
* Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
* Set to `true` to include IndexedDB in the storage state snapshot. If your application uses IndexedDB to store
|
||||||
|
* authentication tokens, like Firebase Authentication, enable this.
|
||||||
|
*
|
||||||
|
* **NOTE** IndexedDBs with typed arrays are currently not supported.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
indexedDB?: boolean;
|
indexedDB?: boolean;
|
||||||
|
|
||||||
|
|
@ -9316,49 +9317,7 @@ export interface BrowserContext {
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
indexedDB: Array<{
|
indexedDB: Array<unknown>;
|
||||||
name: string;
|
|
||||||
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
@ -9741,12 +9700,6 @@ export interface Browser {
|
||||||
*/
|
*/
|
||||||
acceptDownloads?: boolean;
|
acceptDownloads?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),
|
* When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),
|
||||||
* [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
|
* [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
|
||||||
|
|
@ -10135,55 +10088,7 @@ export interface Browser {
|
||||||
/**
|
/**
|
||||||
* indexedDB to set for context
|
* indexedDB to set for context
|
||||||
*/
|
*/
|
||||||
indexedDB?: Array<{
|
indexedDB?: Array<unknown>;
|
||||||
/**
|
|
||||||
* database name
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* database version
|
|
||||||
*/
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -13224,6 +13129,11 @@ export interface Locator {
|
||||||
* `<article><div>Playwright</div></article>`.
|
* `<article><div>Playwright</div></article>`.
|
||||||
*/
|
*/
|
||||||
hasText?: string|RegExp;
|
hasText?: string|RegExp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only matches visible or invisible elements.
|
||||||
|
*/
|
||||||
|
visible?: boolean;
|
||||||
}): Locator;
|
}): Locator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -14426,7 +14336,8 @@ export interface Locator {
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a tap gesture on the element matching the locator.
|
* Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
|
||||||
|
* dispatching touch events, see the [emulating legacy touch events](https://playwright.dev/docs/touch-events) page.
|
||||||
*
|
*
|
||||||
* **Details**
|
* **Details**
|
||||||
*
|
*
|
||||||
|
|
@ -14812,12 +14723,6 @@ export interface BrowserType<Unused = {}> {
|
||||||
*/
|
*/
|
||||||
acceptDownloads?: boolean;
|
acceptDownloads?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
|
* **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
|
||||||
*
|
*
|
||||||
|
|
@ -16708,12 +16613,6 @@ export interface AndroidDevice {
|
||||||
*/
|
*/
|
||||||
acceptDownloads?: boolean;
|
acceptDownloads?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
|
* **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
|
||||||
*
|
*
|
||||||
|
|
@ -17560,12 +17459,6 @@ export interface APIRequest {
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
newContext(options?: {
|
newContext(options?: {
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Methods like
|
* Methods like
|
||||||
* [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get)
|
* [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get)
|
||||||
|
|
@ -17641,6 +17534,12 @@ export interface APIRequest {
|
||||||
*/
|
*/
|
||||||
extraHTTPHeaders?: { [key: string]: string; };
|
extraHTTPHeaders?: { [key: string]: string; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status
|
||||||
|
* codes.
|
||||||
|
*/
|
||||||
|
failOnStatusCode?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no
|
* Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no
|
||||||
* origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
* origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||||
|
|
@ -17742,55 +17641,7 @@ export interface APIRequest {
|
||||||
/**
|
/**
|
||||||
* indexedDB to set for context
|
* indexedDB to set for context
|
||||||
*/
|
*/
|
||||||
indexedDB?: Array<{
|
indexedDB?: Array<unknown>;
|
||||||
/**
|
|
||||||
* database name
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* database version
|
|
||||||
*/
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18564,7 +18415,7 @@ export interface APIRequestContext {
|
||||||
*/
|
*/
|
||||||
storageState(options?: {
|
storageState(options?: {
|
||||||
/**
|
/**
|
||||||
* Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
* Set to `true` to include IndexedDB in the storage state snapshot.
|
||||||
*/
|
*/
|
||||||
indexedDB?: boolean;
|
indexedDB?: boolean;
|
||||||
|
|
||||||
|
|
@ -18606,49 +18457,7 @@ export interface APIRequestContext {
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
indexedDB: Array<{
|
indexedDB: Array<unknown>;
|
||||||
name: string;
|
|
||||||
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
@ -21310,6 +21119,9 @@ export interface Selectors {
|
||||||
/**
|
/**
|
||||||
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on
|
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on
|
||||||
* the touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true.
|
* the touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true.
|
||||||
|
*
|
||||||
|
* This class is limited to emulating tap gestures. For examples of other gestures simulated by manually dispatching
|
||||||
|
* touch events, see the [emulating legacy touch events](https://playwright.dev/docs/touch-events) page.
|
||||||
*/
|
*/
|
||||||
export interface Touchscreen {
|
export interface Touchscreen {
|
||||||
/**
|
/**
|
||||||
|
|
@ -22142,12 +21954,6 @@ export interface BrowserContextOptions {
|
||||||
*/
|
*/
|
||||||
acceptDownloads?: boolean;
|
acceptDownloads?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By
|
|
||||||
* default, response object is returned for all status codes.
|
|
||||||
*/
|
|
||||||
apiRequestFailsOnErrorStatus?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),
|
* When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),
|
||||||
* [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
|
* [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
|
||||||
|
|
@ -22503,55 +22309,7 @@ export interface BrowserContextOptions {
|
||||||
/**
|
/**
|
||||||
* indexedDB to set for context
|
* indexedDB to set for context
|
||||||
*/
|
*/
|
||||||
indexedDB?: Array<{
|
indexedDB?: Array<unknown>;
|
||||||
/**
|
|
||||||
* database name
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* database version
|
|
||||||
*/
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
stores: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
autoIncrement: boolean;
|
|
||||||
|
|
||||||
indexes: Array<{
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
keyPath?: string;
|
|
||||||
|
|
||||||
keyPathArray?: Array<string>;
|
|
||||||
|
|
||||||
unique: boolean;
|
|
||||||
|
|
||||||
multiEntry: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
records: Array<{
|
|
||||||
key?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
keyEncoded?: Object;
|
|
||||||
|
|
||||||
value?: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
|
||||||
*/
|
|
||||||
valueEncoded?: Object;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ export class FullConfigInternal {
|
||||||
readonly plugins: TestRunnerPluginRegistration[];
|
readonly plugins: TestRunnerPluginRegistration[];
|
||||||
readonly projects: FullProjectInternal[] = [];
|
readonly projects: FullProjectInternal[] = [];
|
||||||
readonly singleTSConfigPath?: string;
|
readonly singleTSConfigPath?: string;
|
||||||
readonly populateGitInfo: boolean;
|
|
||||||
readonly recreateWorkerAfterFailure: boolean;
|
readonly recreateWorkerAfterFailure: boolean;
|
||||||
cliArgs: string[] = [];
|
cliArgs: string[] = [];
|
||||||
cliGrep: string | undefined;
|
cliGrep: string | undefined;
|
||||||
|
|
@ -79,7 +78,6 @@ export class FullConfigInternal {
|
||||||
const privateConfiguration = (userConfig as any)['@playwright/test'];
|
const privateConfiguration = (userConfig as any)['@playwright/test'];
|
||||||
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
|
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
|
||||||
this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig);
|
this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig);
|
||||||
this.populateGitInfo = takeFirst(userConfig.populateGitInfo, defaultPopulateGitInfo);
|
|
||||||
|
|
||||||
this.globalSetups = (Array.isArray(userConfig.globalSetup) ? userConfig.globalSetup : [userConfig.globalSetup]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined);
|
this.globalSetups = (Array.isArray(userConfig.globalSetup) ? userConfig.globalSetup : [userConfig.globalSetup]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined);
|
||||||
this.globalTeardowns = (Array.isArray(userConfig.globalTeardown) ? userConfig.globalTeardown : [userConfig.globalTeardown]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined);
|
this.globalTeardowns = (Array.isArray(userConfig.globalTeardown) ? userConfig.globalTeardown : [userConfig.globalTeardown]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined);
|
||||||
|
|
@ -97,14 +95,14 @@ export class FullConfigInternal {
|
||||||
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false),
|
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false),
|
||||||
globalSetup: this.globalSetups[0] ?? null,
|
globalSetup: this.globalSetups[0] ?? null,
|
||||||
globalTeardown: this.globalTeardowns[0] ?? null,
|
globalTeardown: this.globalTeardowns[0] ?? null,
|
||||||
globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
|
globalTimeout: takeFirst(configCLIOverrides.debug ? 0 : undefined, configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
|
||||||
grep: takeFirst(userConfig.grep, defaultGrep),
|
grep: takeFirst(userConfig.grep, defaultGrep),
|
||||||
grepInvert: takeFirst(userConfig.grepInvert, null),
|
grepInvert: takeFirst(userConfig.grepInvert, null),
|
||||||
maxFailures: takeFirst(configCLIOverrides.debug ? 1 : undefined, configCLIOverrides.maxFailures, userConfig.maxFailures, 0),
|
maxFailures: takeFirst(configCLIOverrides.debug ? 1 : undefined, configCLIOverrides.maxFailures, userConfig.maxFailures, 0),
|
||||||
metadata: userConfig.metadata,
|
metadata: userConfig.metadata,
|
||||||
preserveOutput: takeFirst(userConfig.preserveOutput, 'always'),
|
preserveOutput: takeFirst(userConfig.preserveOutput, 'always'),
|
||||||
reporter: takeFirst(configCLIOverrides.reporter, resolveReporters(userConfig.reporter, configDir), [[defaultReporter]]),
|
reporter: takeFirst(configCLIOverrides.reporter, resolveReporters(userConfig.reporter, configDir), [[defaultReporter]]),
|
||||||
reportSlowTests: takeFirst(userConfig.reportSlowTests, { max: 5, threshold: 15000 }),
|
reportSlowTests: takeFirst(userConfig.reportSlowTests, { max: 5, threshold: 300_000 /* 5 minutes */ }),
|
||||||
quiet: takeFirst(configCLIOverrides.quiet, userConfig.quiet, false),
|
quiet: takeFirst(configCLIOverrides.quiet, userConfig.quiet, false),
|
||||||
projects: [],
|
projects: [],
|
||||||
shard: takeFirst(configCLIOverrides.shard, userConfig.shard, null),
|
shard: takeFirst(configCLIOverrides.shard, userConfig.shard, null),
|
||||||
|
|
@ -304,7 +302,6 @@ function resolveScript(id: string | undefined, rootDir: string): string | undefi
|
||||||
|
|
||||||
export const defaultGrep = /.*/;
|
export const defaultGrep = /.*/;
|
||||||
export const defaultReporter = process.env.CI ? 'dot' : 'list';
|
export const defaultReporter = process.env.CI ? 'dot' : 'list';
|
||||||
const defaultPopulateGitInfo = process.env.GITHUB_ACTIONS === 'true';
|
|
||||||
|
|
||||||
const configInternalSymbol = Symbol('configInternalSymbol');
|
const configInternalSymbol = Symbol('configInternalSymbol');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -628,7 +628,7 @@ class ArtifactsRecorder {
|
||||||
await page.screenshot({ ...screenshotOptions, timeout: 5000, path, caret: 'initial' });
|
await page.screenshot({ ...screenshotOptions, timeout: 5000, path, caret: 'initial' });
|
||||||
});
|
});
|
||||||
|
|
||||||
this._pageSnapshotRecorder = new SnapshotRecorder(this, pageSnapshot, 'pageSnapshot', 'text/plain', '.ariasnapshot', async (page, path) => {
|
this._pageSnapshotRecorder = new SnapshotRecorder(this, pageSnapshot, 'pageSnapshot', 'text/plain', '.snapshot.yml', async (page, path) => {
|
||||||
const ariaSnapshot = await page.locator('body').ariaSnapshot({ timeout: 5000 });
|
const ariaSnapshot = await page.locator('body').ariaSnapshot({ timeout: 5000 });
|
||||||
await fs.promises.writeFile(path, ariaSnapshot);
|
await fs.promises.writeFile(path, ariaSnapshot);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -605,7 +605,7 @@ export const baseFullConfig: reporterTypes.FullConfig = {
|
||||||
preserveOutput: 'always',
|
preserveOutput: 'always',
|
||||||
projects: [],
|
projects: [],
|
||||||
reporter: [[process.env.CI ? 'dot' : 'list']],
|
reporter: [[process.env.CI ? 'dot' : 'list']],
|
||||||
reportSlowTests: { max: 5, threshold: 15000 },
|
reportSlowTests: { max: 5, threshold: 300_000 /* 5 minutes */ },
|
||||||
configFile: '',
|
configFile: '',
|
||||||
rootDir: '',
|
rootDir: '',
|
||||||
quiet: false,
|
quiet: false,
|
||||||
|
|
|
||||||
53
packages/playwright/src/isomorphic/types.d.ts
vendored
53
packages/playwright/src/isomorphic/types.d.ts
vendored
|
|
@ -14,17 +14,42 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface GitCommitInfo {
|
export type GitCommitInfo = {
|
||||||
'revision.id'?: string;
|
shortHash: string;
|
||||||
'revision.author'?: string;
|
hash: string;
|
||||||
'revision.email'?: string;
|
subject: string;
|
||||||
'revision.subject'?: string;
|
body: string;
|
||||||
'revision.timestamp'?: number | Date;
|
author: {
|
||||||
'revision.link'?: string;
|
name: string;
|
||||||
'revision.diff'?: string;
|
email: string;
|
||||||
'pull.link'?: string;
|
time: number;
|
||||||
'pull.diff'?: string;
|
};
|
||||||
'pull.base'?: string;
|
committer: {
|
||||||
'pull.title'?: string;
|
name: string;
|
||||||
'ci.link'?: string;
|
email: string
|
||||||
}
|
time: number;
|
||||||
|
};
|
||||||
|
branch: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CIInfo = {
|
||||||
|
commitHref: string;
|
||||||
|
prHref?: string;
|
||||||
|
prTitle?: string;
|
||||||
|
buildHref?: string;
|
||||||
|
commitHash?: string;
|
||||||
|
baseHash?: string;
|
||||||
|
branch?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserMetadataWithCommitInfo = {
|
||||||
|
ci?: CIInfo;
|
||||||
|
gitCommit?: GitCommitInfo | 'generate';
|
||||||
|
gitDiff?: string | 'generate';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MetadataWithCommitInfo = {
|
||||||
|
ci?: CIInfo;
|
||||||
|
gitCommit?: GitCommitInfo;
|
||||||
|
gitDiff?: string;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import { escapeTemplateString, isString, sanitizeForFilePath } from 'playwright-
|
||||||
|
|
||||||
import { kNoElementsFoundError, matcherHint } from './matcherHint';
|
import { kNoElementsFoundError, matcherHint } from './matcherHint';
|
||||||
import { EXPECTED_COLOR } from '../common/expectBundle';
|
import { EXPECTED_COLOR } from '../common/expectBundle';
|
||||||
import { callLogText, sanitizeFilePathBeforeExtension, trimLongString } from '../util';
|
import { callLogText, fileExistsAsync, sanitizeFilePathBeforeExtension, trimLongString } from '../util';
|
||||||
import { printReceivedStringContainExpectedSubstring } from './expect';
|
import { printReceivedStringContainExpectedSubstring } from './expect';
|
||||||
import { currentTestInfo } from '../common/globals';
|
import { currentTestInfo } from '../common/globals';
|
||||||
|
|
||||||
|
|
@ -70,7 +70,8 @@ export async function toMatchAriaSnapshot(
|
||||||
timeout = options.timeout ?? this.timeout;
|
timeout = options.timeout ?? this.timeout;
|
||||||
} else {
|
} else {
|
||||||
if (expectedParam?.name) {
|
if (expectedParam?.name) {
|
||||||
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeFilePathBeforeExtension(expectedParam.name)]);
|
const ext = expectedParam.name!.endsWith('.snapshot.yml') ? '.snapshot.yml' : undefined;
|
||||||
|
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeFilePathBeforeExtension(expectedParam.name, ext)]);
|
||||||
} else {
|
} else {
|
||||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||||
if (!snapshotNames) {
|
if (!snapshotNames) {
|
||||||
|
|
@ -78,7 +79,14 @@ export async function toMatchAriaSnapshot(
|
||||||
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
||||||
}
|
}
|
||||||
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
|
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
|
||||||
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml']);
|
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec))], '.snapshot.yml');
|
||||||
|
// in 1.51, we changed the default template to use .snapshot.yml extension
|
||||||
|
// for backwards compatibility, we check for the legacy .yml extension
|
||||||
|
if (!(await fileExistsAsync(expectedPath))) {
|
||||||
|
const legacyPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec))], '.yml');
|
||||||
|
if (await fileExistsAsync(legacyPath))
|
||||||
|
expectedPath = legacyPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
|
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
|
||||||
timeout = expectedParam?.timeout ?? this.timeout;
|
timeout = expectedParam?.timeout ?? this.timeout;
|
||||||
|
|
|
||||||
|
|
@ -14,108 +14,151 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
|
||||||
import { createGuid, spawnAsync } from 'playwright-core/lib/utils';
|
import { spawnAsync } from 'playwright-core/lib/utils';
|
||||||
|
|
||||||
import type { TestRunnerPlugin } from './';
|
import type { TestRunnerPlugin } from './';
|
||||||
import type { FullConfig } from '../../types/testReporter';
|
import type { FullConfig } from '../../types/testReporter';
|
||||||
import type { FullConfigInternal } from '../common/config';
|
import type { FullConfigInternal } from '../common/config';
|
||||||
import type { GitCommitInfo } from '../isomorphic/types';
|
import type { GitCommitInfo, CIInfo, UserMetadataWithCommitInfo } from '../isomorphic/types';
|
||||||
|
|
||||||
const GIT_OPERATIONS_TIMEOUT_MS = 1500;
|
const GIT_OPERATIONS_TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
export const addGitCommitInfoPlugin = (fullConfig: FullConfigInternal) => {
|
export const addGitCommitInfoPlugin = (fullConfig: FullConfigInternal) => {
|
||||||
if (fullConfig.populateGitInfo)
|
fullConfig.plugins.push({ factory: gitCommitInfoPlugin });
|
||||||
fullConfig.plugins.push({ factory: gitCommitInfo });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
|
type GitCommitInfoPluginOptions = {
|
||||||
|
directory?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gitCommitInfoPlugin = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
|
||||||
return {
|
return {
|
||||||
name: 'playwright:git-commit-info',
|
name: 'playwright:git-commit-info',
|
||||||
|
|
||||||
setup: async (config: FullConfig, configDir: string) => {
|
setup: async (config: FullConfig, configDir: string) => {
|
||||||
const fromEnv = await linksFromEnv();
|
const metadata = config.metadata as UserMetadataWithCommitInfo;
|
||||||
const fromCLI = await gitStatusFromCLI(options?.directory || configDir, fromEnv);
|
const ci = await ciInfo();
|
||||||
config.metadata = config.metadata || {};
|
if (!metadata.ci && ci)
|
||||||
config.metadata['git.commit.info'] = { ...fromEnv, ...fromCLI };
|
metadata.ci = ci;
|
||||||
|
|
||||||
|
if ((ci && !metadata.gitCommit) || metadata.gitCommit === 'generate') {
|
||||||
|
const git = await gitCommitInfo(options?.directory || configDir).catch(e => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to get git commit info', e);
|
||||||
|
});
|
||||||
|
if (git)
|
||||||
|
metadata.gitCommit = git;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ci && !metadata.gitDiff) || metadata.gitDiff === 'generate') {
|
||||||
|
const diffResult = await gitDiff(options?.directory || configDir, ci).catch(e => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to get git diff', e);
|
||||||
|
});
|
||||||
|
if (diffResult)
|
||||||
|
metadata.gitDiff = diffResult;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface GitCommitInfoPluginOptions {
|
async function ciInfo(): Promise<CIInfo | undefined> {
|
||||||
directory?: string;
|
if (process.env.GITHUB_ACTIONS) {
|
||||||
}
|
let pr: { title: string, number: number } | undefined;
|
||||||
|
|
||||||
async function linksFromEnv() {
|
|
||||||
const out: Partial<GitCommitInfo> = {};
|
|
||||||
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
|
||||||
if (process.env.BUILD_URL)
|
|
||||||
out['ci.link'] = process.env.BUILD_URL;
|
|
||||||
// GitLab: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
|
|
||||||
if (process.env.CI_PROJECT_URL && process.env.CI_COMMIT_SHA)
|
|
||||||
out['revision.link'] = `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`;
|
|
||||||
if (process.env.CI_JOB_URL)
|
|
||||||
out['ci.link'] = process.env.CI_JOB_URL;
|
|
||||||
// GitHub: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
|
|
||||||
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_SHA)
|
|
||||||
out['revision.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`;
|
|
||||||
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
|
|
||||||
out['ci.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
||||||
if (process.env.GITHUB_EVENT_PATH) {
|
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(await fs.promises.readFile(process.env.GITHUB_EVENT_PATH, 'utf8'));
|
const json = JSON.parse(await fs.promises.readFile(process.env.GITHUB_EVENT_PATH!, 'utf8'));
|
||||||
if (json.pull_request) {
|
pr = { title: json.pull_request.title, number: json.pull_request.number };
|
||||||
out['pull.title'] = json.pull_request.title;
|
|
||||||
out['pull.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${json.pull_request.number}`;
|
|
||||||
out['pull.base'] = json.pull_request.base.ref;
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
commitHref: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`,
|
||||||
|
prHref: pr ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${pr.number}` : undefined,
|
||||||
|
prTitle: pr ? pr.title : undefined,
|
||||||
|
buildHref: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`,
|
||||||
|
commitHash: process.env.GITHUB_SHA,
|
||||||
|
baseHash: process.env.GITHUB_BASE_REF,
|
||||||
|
branch: process.env.GITHUB_REF_NAME,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return out;
|
|
||||||
|
if (process.env.GITLAB_CI) {
|
||||||
|
return {
|
||||||
|
commitHref: `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`,
|
||||||
|
prHref: process.env.CI_MERGE_REQUEST_IID ? `${process.env.CI_PROJECT_URL}/-/merge_requests/${process.env.CI_MERGE_REQUEST_IID}` : undefined,
|
||||||
|
buildHref: process.env.CI_JOB_URL,
|
||||||
|
commitHash: process.env.CI_COMMIT_SHA,
|
||||||
|
baseHash: process.env.CI_COMMIT_BEFORE_SHA,
|
||||||
|
branch: process.env.CI_COMMIT_REF_NAME,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.JENKINS_URL && process.env.BUILD_URL) {
|
||||||
|
return {
|
||||||
|
commitHref: process.env.BUILD_URL,
|
||||||
|
commitHash: process.env.GIT_COMMIT,
|
||||||
|
baseHash: process.env.GIT_PREVIOUS_COMMIT,
|
||||||
|
branch: process.env.GIT_BRANCH,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open to PRs.
|
||||||
}
|
}
|
||||||
|
|
||||||
async function gitStatusFromCLI(gitDir: string, envInfo: Pick<GitCommitInfo, 'pull.base'>): Promise<GitCommitInfo | undefined> {
|
async function gitCommitInfo(gitDir: string): Promise<GitCommitInfo | undefined> {
|
||||||
const separator = `:${createGuid().slice(0, 4)}:`;
|
const separator = `---786eec917292---`;
|
||||||
|
const tokens = [
|
||||||
|
'%H', // commit hash
|
||||||
|
'%h', // abbreviated commit hash
|
||||||
|
'%s', // subject
|
||||||
|
'%B', // raw body (unwrapped subject and body)
|
||||||
|
'%an', // author name
|
||||||
|
'%ae', // author email
|
||||||
|
'%at', // author date, UNIX timestamp
|
||||||
|
'%cn', // committer name
|
||||||
|
'%ce', // committer email
|
||||||
|
'%ct', // committer date, UNIX timestamp
|
||||||
|
'', // branch
|
||||||
|
];
|
||||||
const commitInfoResult = await spawnAsync(
|
const commitInfoResult = await spawnAsync(
|
||||||
'git',
|
`git log -1 --pretty=format:"${tokens.join(separator)}" && git rev-parse --abbrev-ref HEAD`, [],
|
||||||
['show', '-s', `--format=%H${separator}%s${separator}%an${separator}%ae${separator}%ct`, 'HEAD'],
|
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS, shell: true }
|
||||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
|
||||||
);
|
);
|
||||||
if (commitInfoResult.code)
|
if (commitInfoResult.code)
|
||||||
return;
|
return undefined;
|
||||||
const showOutput = commitInfoResult.stdout.trim();
|
const showOutput = commitInfoResult.stdout.trim();
|
||||||
const [id, subject, author, email, rawTimestamp] = showOutput.split(separator);
|
const [hash, shortHash, subject, body, authorName, authorEmail, authorTime, committerName, committerEmail, committerTime, branch] = showOutput.split(separator);
|
||||||
let timestamp: number = Number.parseInt(rawTimestamp, 10);
|
|
||||||
timestamp = Number.isInteger(timestamp) ? timestamp * 1000 : 0;
|
|
||||||
|
|
||||||
const result: GitCommitInfo = {
|
return {
|
||||||
'revision.id': id,
|
shortHash,
|
||||||
'revision.author': author,
|
hash,
|
||||||
'revision.email': email,
|
subject,
|
||||||
'revision.subject': subject,
|
body,
|
||||||
'revision.timestamp': timestamp,
|
author: {
|
||||||
|
name: authorName,
|
||||||
|
email: authorEmail,
|
||||||
|
time: +authorTime * 1000,
|
||||||
|
},
|
||||||
|
committer: {
|
||||||
|
name: committerName,
|
||||||
|
email: committerEmail,
|
||||||
|
time: +committerTime * 1000,
|
||||||
|
},
|
||||||
|
branch: branch.trim(),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
const diffLimit = 1_000_000; // 1MB
|
|
||||||
if (envInfo['pull.base']) {
|
async function gitDiff(gitDir: string, ci?: CIInfo): Promise<string | undefined> {
|
||||||
const pullDiffResult = await spawnAsync(
|
const diffLimit = 100_000;
|
||||||
'git',
|
const baseHash = ci?.baseHash ?? 'HEAD~1';
|
||||||
['diff', envInfo['pull.base']],
|
|
||||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
const pullDiffResult = await spawnAsync(
|
||||||
);
|
'git',
|
||||||
if (!pullDiffResult.code)
|
['diff', baseHash],
|
||||||
result['pull.diff'] = pullDiffResult.stdout.substring(0, diffLimit);
|
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||||
} else {
|
);
|
||||||
const diffResult = await spawnAsync(
|
if (!pullDiffResult.code)
|
||||||
'git',
|
return pullDiffResult.stdout.substring(0, diffLimit);
|
||||||
['diff', 'HEAD~1'],
|
|
||||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
|
||||||
);
|
|
||||||
if (!diffResult.code)
|
|
||||||
result['revision.diff'] = diffResult.stdout.substring(0, diffLimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,3 @@ export type TestRunnerPluginRegistration = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export { webServer } from './webServerPlugin';
|
export { webServer } from './webServerPlugin';
|
||||||
export { gitCommitInfo } from './gitCommitInfoPlugin';
|
|
||||||
|
|
|
||||||
|
|
@ -206,8 +206,8 @@ export function addSuffixToFilePath(filePath: string, suffix: string): string {
|
||||||
return base + suffix + ext;
|
return base + suffix + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeFilePathBeforeExtension(filePath: string): string {
|
export function sanitizeFilePathBeforeExtension(filePath: string, ext?: string): string {
|
||||||
const ext = path.extname(filePath);
|
ext ??= path.extname(filePath);
|
||||||
const base = filePath.substring(0, filePath.length - ext.length);
|
const base = filePath.substring(0, filePath.length - ext.length);
|
||||||
return sanitizeForFilePath(base) + ext;
|
return sanitizeForFilePath(base) + ext;
|
||||||
}
|
}
|
||||||
|
|
@ -391,6 +391,15 @@ function fileExists(resolved: string) {
|
||||||
return fs.statSync(resolved, { throwIfNoEntry: false })?.isFile();
|
return fs.statSync(resolved, { throwIfNoEntry: false })?.isFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fileExistsAsync(resolved: string) {
|
||||||
|
try {
|
||||||
|
const stat = await fs.promises.stat(resolved);
|
||||||
|
return stat.isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function dirExists(resolved: string) {
|
function dirExists(resolved: string) {
|
||||||
return fs.statSync(resolved, { throwIfNoEntry: false })?.isDirectory();
|
return fs.statSync(resolved, { throwIfNoEntry: false })?.isDirectory();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class Fixture {
|
||||||
private _selfTeardownComplete: Promise<void> | undefined;
|
private _selfTeardownComplete: Promise<void> | undefined;
|
||||||
private _setupDescription: FixtureDescription;
|
private _setupDescription: FixtureDescription;
|
||||||
private _teardownDescription: FixtureDescription;
|
private _teardownDescription: FixtureDescription;
|
||||||
private _stepInfo: { category: 'fixture', location?: Location } | undefined;
|
private _stepInfo: { title: string, category: 'fixture', location?: Location } | undefined;
|
||||||
_deps = new Set<Fixture>();
|
_deps = new Set<Fixture>();
|
||||||
_usages = new Set<Fixture>();
|
_usages = new Set<Fixture>();
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ class Fixture {
|
||||||
const isUserFixture = this.registration.location && filterStackFile(this.registration.location.file);
|
const isUserFixture = this.registration.location && filterStackFile(this.registration.location.file);
|
||||||
const title = this.registration.customTitle || this.registration.name;
|
const title = this.registration.customTitle || this.registration.name;
|
||||||
const location = isUserFixture ? this.registration.location : undefined;
|
const location = isUserFixture ? this.registration.location : undefined;
|
||||||
this._stepInfo = shouldGenerateStep ? { category: 'fixture', location } : undefined;
|
this._stepInfo = shouldGenerateStep ? { title: `fixture: ${title}`, category: 'fixture', location } : undefined;
|
||||||
this._setupDescription = {
|
this._setupDescription = {
|
||||||
title,
|
title,
|
||||||
phase: 'setup',
|
phase: 'setup',
|
||||||
|
|
@ -68,13 +68,11 @@ class Fixture {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await testInfo._runAsStage({
|
const run = () => testInfo._runWithTimeout({ ...runnable, fixture: this._setupDescription }, () => this._setupInternal(testInfo));
|
||||||
title: `fixture: ${this.registration.customTitle ?? this.registration.name}`,
|
if (this._stepInfo)
|
||||||
runnable: { ...runnable, fixture: this._setupDescription },
|
await testInfo._runAsStep(this._stepInfo, run);
|
||||||
stepInfo: this._stepInfo,
|
else
|
||||||
}, async () => {
|
await run();
|
||||||
await this._setupInternal(testInfo);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _setupInternal(testInfo: TestInfoImpl) {
|
private async _setupInternal(testInfo: TestInfoImpl) {
|
||||||
|
|
@ -133,13 +131,11 @@ class Fixture {
|
||||||
// Do not even start the teardown for a fixture that does not have any
|
// Do not even start the teardown for a fixture that does not have any
|
||||||
// time remaining in the time slot. This avoids cascading timeouts.
|
// time remaining in the time slot. This avoids cascading timeouts.
|
||||||
if (!testInfo._timeoutManager.isTimeExhaustedFor(fixtureRunnable)) {
|
if (!testInfo._timeoutManager.isTimeExhaustedFor(fixtureRunnable)) {
|
||||||
await testInfo._runAsStage({
|
const run = () => testInfo._runWithTimeout(fixtureRunnable, () => this._teardownInternal());
|
||||||
title: `fixture: ${this.registration.customTitle ?? this.registration.name}`,
|
if (this._stepInfo)
|
||||||
runnable: fixtureRunnable,
|
await testInfo._runAsStep(this._stepInfo, run);
|
||||||
stepInfo: this._stepInfo,
|
else
|
||||||
}, async () => {
|
await run();
|
||||||
await this._teardownInternal();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// To preserve fixtures integrity, forcefully cleanup fixtures
|
// To preserve fixtures integrity, forcefully cleanup fixtures
|
||||||
|
|
@ -268,9 +264,7 @@ export class FixtureRunner {
|
||||||
// Do not run the function when fixture setup has already failed.
|
// Do not run the function when fixture setup has already failed.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
await testInfo._runAsStage({ title: 'run function', runnable }, async () => {
|
await testInfo._runWithTimeout(runnable, () => fn(params, testInfo));
|
||||||
await fn(params, testInfo);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _setupFixtureForRegistration(registration: FixtureRegistration, testInfo: TestInfoImpl, runnable: RunnableDescription): Promise<Fixture> {
|
private async _setupFixtureForRegistration(registration: FixtureRegistration, testInfo: TestInfoImpl, runnable: RunnableDescription): Promise<Fixture> {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import path from 'path';
|
||||||
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils';
|
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils';
|
||||||
|
|
||||||
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
|
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
|
||||||
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
import { filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
||||||
import { TestTracing } from './testTracing';
|
import { TestTracing } from './testTracing';
|
||||||
import { testInfoError } from './util';
|
import { testInfoError } from './util';
|
||||||
import { FloatingPromiseScope } from './floatingPromiseScope';
|
import { FloatingPromiseScope } from './floatingPromiseScope';
|
||||||
|
|
@ -50,16 +50,8 @@ export interface TestStepInternal {
|
||||||
error?: TestInfoErrorImpl;
|
error?: TestInfoErrorImpl;
|
||||||
infectParentStepsWithError?: boolean;
|
infectParentStepsWithError?: boolean;
|
||||||
box?: boolean;
|
box?: boolean;
|
||||||
isStage?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TestStage = {
|
|
||||||
title: string;
|
|
||||||
stepInfo?: { category: 'hook' | 'fixture', location?: Location };
|
|
||||||
runnable?: RunnableDescription;
|
|
||||||
step?: TestStepInternal;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TestInfoImpl implements TestInfo {
|
export class TestInfoImpl implements TestInfo {
|
||||||
private _onStepBegin: (payload: StepBeginPayload) => void;
|
private _onStepBegin: (payload: StepBeginPayload) => void;
|
||||||
private _onStepEnd: (payload: StepEndPayload) => void;
|
private _onStepEnd: (payload: StepEndPayload) => void;
|
||||||
|
|
@ -235,28 +227,27 @@ export class TestInfoImpl implements TestInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _findLastStageStep(steps: TestStepInternal[]): TestStepInternal | undefined {
|
private _findLastPredefinedStep(steps: TestStepInternal[]): TestStepInternal | undefined {
|
||||||
// Find the deepest step that is marked as isStage and has not finished yet.
|
// Find the deepest predefined step that has not finished yet.
|
||||||
for (let i = steps.length - 1; i >= 0; i--) {
|
for (let i = steps.length - 1; i >= 0; i--) {
|
||||||
const child = this._findLastStageStep(steps[i].steps);
|
const child = this._findLastPredefinedStep(steps[i].steps);
|
||||||
if (child)
|
if (child)
|
||||||
return child;
|
return child;
|
||||||
if (steps[i].isStage && !steps[i].endWallTime)
|
if ((steps[i].category === 'hook' || steps[i].category === 'fixture') && !steps[i].endWallTime)
|
||||||
return steps[i];
|
return steps[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parentStep() {
|
private _parentStep() {
|
||||||
return currentZone().data<TestStepInternal>('stepZone')
|
return currentZone().data<TestStepInternal>('stepZone') ?? this._findLastPredefinedStep(this._steps);
|
||||||
?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices' | 'info'>, parentStep?: TestStepInternal): TestStepInternal {
|
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices' | 'info'>, parentStep?: TestStepInternal): TestStepInternal {
|
||||||
const stepId = `${data.category}@${++this._lastStepId}`;
|
const stepId = `${data.category}@${++this._lastStepId}`;
|
||||||
|
|
||||||
if (data.isStage) {
|
if (data.category === 'hook' || data.category === 'fixture') {
|
||||||
// Predefined stages form a fixed hierarchy - use the current one as parent.
|
// Predefined steps form a fixed hierarchy - use the current one as parent.
|
||||||
parentStep = this._findLastStageStep(this._steps);
|
parentStep = this._findLastPredefinedStep(this._steps);
|
||||||
} else {
|
} else {
|
||||||
if (!parentStep)
|
if (!parentStep)
|
||||||
parentStep = this._parentStep();
|
parentStep = this._parentStep();
|
||||||
|
|
@ -355,21 +346,23 @@ export class TestInfoImpl implements TestInfo {
|
||||||
this._tracing.appendForError(serialized);
|
this._tracing.appendForError(serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _runAsStage(stage: TestStage, cb: () => Promise<any>) {
|
async _runAsStep(stepInfo: { title: string, category: 'hook' | 'fixture', location?: Location }, cb: () => Promise<any>) {
|
||||||
if (debugTest.enabled) {
|
const step = this._addStep(stepInfo);
|
||||||
const location = stage.runnable?.location ? ` at "${formatLocation(stage.runnable.location)}"` : ``;
|
|
||||||
debugTest(`started stage "${stage.title}"${location}`);
|
|
||||||
}
|
|
||||||
stage.step = stage.stepInfo ? this._addStep({ ...stage.stepInfo, title: stage.title, isStage: true }) : undefined;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._timeoutManager.withRunnable(stage.runnable, async () => {
|
await cb();
|
||||||
|
step.complete({});
|
||||||
|
} catch (error) {
|
||||||
|
step.complete({ error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _runWithTimeout(runnable: RunnableDescription, cb: () => Promise<any>) {
|
||||||
|
try {
|
||||||
|
await this._timeoutManager.withRunnable(runnable, async () => {
|
||||||
try {
|
try {
|
||||||
await cb();
|
await cb();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Only handle errors directly thrown by the user code.
|
|
||||||
if (!stage.runnable)
|
|
||||||
throw e;
|
|
||||||
if (this._allowSkips && (e instanceof SkipError)) {
|
if (this._allowSkips && (e instanceof SkipError)) {
|
||||||
if (this.status === 'passed')
|
if (this.status === 'passed')
|
||||||
this.status = 'skipped';
|
this.status = 'skipped';
|
||||||
|
|
@ -377,7 +370,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
// Unfortunately, we have to handle user errors and timeout errors differently.
|
// Unfortunately, we have to handle user errors and timeout errors differently.
|
||||||
// Consider the following scenario:
|
// Consider the following scenario:
|
||||||
// - locator.click times out
|
// - locator.click times out
|
||||||
// - all stages containing the test function finish with TimeoutManagerError
|
// - all steps containing the test function finish with TimeoutManagerError
|
||||||
// - test finishes, the page is closed and this triggers locator.click error
|
// - test finishes, the page is closed and this triggers locator.click error
|
||||||
// - we would like to present the locator.click error to the user
|
// - we would like to present the locator.click error to the user
|
||||||
// - therefore, we need a try/catch inside the "run with timeout" block and capture the error
|
// - therefore, we need a try/catch inside the "run with timeout" block and capture the error
|
||||||
|
|
@ -386,16 +379,12 @@ export class TestInfoImpl implements TestInfo {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
stage.step?.complete({});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// When interrupting, we arrive here with a TimeoutManagerError, but we should not
|
// When interrupting, we arrive here with a TimeoutManagerError, but we should not
|
||||||
// consider it a timeout.
|
// consider it a timeout.
|
||||||
if (!this._wasInterrupted && (error instanceof TimeoutManagerError) && stage.runnable)
|
if (!this._wasInterrupted && (error instanceof TimeoutManagerError))
|
||||||
this._failWithError(error);
|
this._failWithError(error);
|
||||||
stage.step?.complete({ error });
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
debugTest(`finished stage "${stage.title}"`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,7 +419,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
} else {
|
} else {
|
||||||
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
|
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
|
||||||
const callId = `attach@${++this._lastStepId}`;
|
const callId = `attach@${++this._lastStepId}`;
|
||||||
this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, 'attach', `attach "${attachment.name}"`, undefined, []);
|
this._tracing.appendBeforeActionForStep(callId, this._findLastPredefinedStep(this._steps)?.stepId, 'attach', `attach "${attachment.name}"`, undefined, []);
|
||||||
this._tracing.appendAfterActionForStep(callId, undefined, [attachment]);
|
this._tracing.appendAfterActionForStep(callId, undefined, [attachment]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -463,9 +452,11 @@ export class TestInfoImpl implements TestInfo {
|
||||||
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
|
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
|
||||||
}
|
}
|
||||||
|
|
||||||
_resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[]) {
|
_resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[], extension?: string) {
|
||||||
const subPath = path.join(...pathSegments);
|
const subPath = path.join(...pathSegments);
|
||||||
const parsedSubPath = path.parse(subPath);
|
const dir = path.dirname(subPath);
|
||||||
|
const ext = extension ?? path.extname(subPath);
|
||||||
|
const name = path.basename(subPath, ext);
|
||||||
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
|
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
|
||||||
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
||||||
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
|
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
|
||||||
|
|
@ -481,8 +472,8 @@ export class TestInfoImpl implements TestInfo {
|
||||||
.replace(/\{(.)?testName\}/g, '$1' + this._fsSanitizedTestName())
|
.replace(/\{(.)?testName\}/g, '$1' + this._fsSanitizedTestName())
|
||||||
.replace(/\{(.)?testFileName\}/g, '$1' + parsedRelativeTestFilePath.base)
|
.replace(/\{(.)?testFileName\}/g, '$1' + parsedRelativeTestFilePath.base)
|
||||||
.replace(/\{(.)?testFilePath\}/g, '$1' + relativeTestFilePath)
|
.replace(/\{(.)?testFilePath\}/g, '$1' + relativeTestFilePath)
|
||||||
.replace(/\{(.)?arg\}/g, '$1' + path.join(parsedSubPath.dir, parsedSubPath.name))
|
.replace(/\{(.)?arg\}/g, '$1' + path.join(dir, name))
|
||||||
.replace(/\{(.)?ext\}/g, parsedSubPath.ext ? '$1' + parsedSubPath.ext : '');
|
.replace(/\{(.)?ext\}/g, ext ? '$1' + ext : '');
|
||||||
|
|
||||||
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
|
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
import { ManualPromise, monotonicTime } from 'playwright-core/lib/utils';
|
import { ManualPromise, monotonicTime } from 'playwright-core/lib/utils';
|
||||||
import { colors } from 'playwright-core/lib/utils';
|
import { colors } from 'playwright-core/lib/utils';
|
||||||
|
|
||||||
|
import { debugTest, formatLocation } from '../util';
|
||||||
|
|
||||||
import type { Location } from '../../types/testReporter';
|
import type { Location } from '../../types/testReporter';
|
||||||
|
|
||||||
export type TimeSlot = {
|
export type TimeSlot = {
|
||||||
|
|
@ -76,9 +78,7 @@ export class TimeoutManager {
|
||||||
return slot.timeout > 0 && (slot.elapsed >= slot.timeout - 1);
|
return slot.timeout > 0 && (slot.elapsed >= slot.timeout - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async withRunnable<T>(runnable: RunnableDescription | undefined, cb: () => Promise<T>): Promise<T> {
|
async withRunnable<T>(runnable: RunnableDescription, cb: () => Promise<T>): Promise<T> {
|
||||||
if (!runnable)
|
|
||||||
return await cb();
|
|
||||||
if (this._running)
|
if (this._running)
|
||||||
throw new Error(`Internal error: duplicate runnable`);
|
throw new Error(`Internal error: duplicate runnable`);
|
||||||
const running = this._running = {
|
const running = this._running = {
|
||||||
|
|
@ -89,7 +89,13 @@ export class TimeoutManager {
|
||||||
timer: undefined,
|
timer: undefined,
|
||||||
timeoutPromise: new ManualPromise(),
|
timeoutPromise: new ManualPromise(),
|
||||||
};
|
};
|
||||||
|
let debugTitle = '';
|
||||||
try {
|
try {
|
||||||
|
if (debugTest.enabled) {
|
||||||
|
debugTitle = runnable.fixture ? `${runnable.fixture.phase} "${runnable.fixture.title}"` : runnable.type;
|
||||||
|
const location = runnable.location ? ` at "${formatLocation(runnable.location)}"` : ``;
|
||||||
|
debugTest(`started ${debugTitle}${location}`);
|
||||||
|
}
|
||||||
this._updateTimeout(running);
|
this._updateTimeout(running);
|
||||||
return await Promise.race([
|
return await Promise.race([
|
||||||
cb(),
|
cb(),
|
||||||
|
|
@ -101,6 +107,8 @@ export class TimeoutManager {
|
||||||
running.timer = undefined;
|
running.timer = undefined;
|
||||||
running.slot.elapsed += monotonicTime() - running.start;
|
running.slot.elapsed += monotonicTime() - running.start;
|
||||||
this._running = undefined;
|
this._running = undefined;
|
||||||
|
if (debugTest.enabled)
|
||||||
|
debugTest(`finished ${debugTitle}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,12 +115,12 @@ export class WorkerMain extends ProcessRunner {
|
||||||
const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {});
|
const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {});
|
||||||
const runnable = { type: 'teardown' } as const;
|
const runnable = { type: 'teardown' } as const;
|
||||||
// We have to load the project to get the right deadline below.
|
// We have to load the project to get the right deadline below.
|
||||||
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => this._loadIfNeeded()).catch(() => {});
|
await fakeTestInfo._runWithTimeout(runnable, () => this._loadIfNeeded()).catch(() => {});
|
||||||
await this._fixtureRunner.teardownScope('test', fakeTestInfo, runnable).catch(() => {});
|
await this._fixtureRunner.teardownScope('test', fakeTestInfo, runnable).catch(() => {});
|
||||||
await this._fixtureRunner.teardownScope('worker', fakeTestInfo, runnable).catch(() => {});
|
await this._fixtureRunner.teardownScope('worker', fakeTestInfo, runnable).catch(() => {});
|
||||||
// Close any other browsers launched in this process. This includes anything launched
|
// Close any other browsers launched in this process. This includes anything launched
|
||||||
// manually in the test/hooks and internal browsers like Playwright Inspector.
|
// manually in the test/hooks and internal browsers like Playwright Inspector.
|
||||||
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
|
await fakeTestInfo._runWithTimeout(runnable, () => gracefullyCloseAll()).catch(() => {});
|
||||||
this._fatalErrors.push(...fakeTestInfo.errors);
|
this._fatalErrors.push(...fakeTestInfo.errors);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._fatalErrors.push(testInfoError(e));
|
this._fatalErrors.push(testInfoError(e));
|
||||||
|
|
@ -330,8 +330,8 @@ export class WorkerMain extends ProcessRunner {
|
||||||
testInfo._floatingPromiseScope.clear();
|
testInfo._floatingPromiseScope.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
await testInfo._runAsStage({ title: 'setup and test' }, async () => {
|
await (async () => {
|
||||||
await testInfo._runAsStage({ title: 'start tracing', runnable: { type: 'test' } }, async () => {
|
await testInfo._runWithTimeout({ type: 'test' }, async () => {
|
||||||
// Ideally, "trace" would be an config-level option belonging to the
|
// Ideally, "trace" would be an config-level option belonging to the
|
||||||
// test runner instead of a fixture belonging to Playwright.
|
// test runner instead of a fixture belonging to Playwright.
|
||||||
// However, for backwards compatibility, we have to read it from a fixture today.
|
// However, for backwards compatibility, we have to read it from a fixture today.
|
||||||
|
|
@ -356,7 +356,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
await removeFolders([testInfo.outputDir]);
|
await removeFolders([testInfo.outputDir]);
|
||||||
|
|
||||||
let testFunctionParams: object | null = null;
|
let testFunctionParams: object | null = null;
|
||||||
await testInfo._runAsStage({ title: 'Before Hooks', stepInfo: { category: 'hook' } }, async () => {
|
await testInfo._runAsStep({ title: 'Before Hooks', category: 'hook' }, async () => {
|
||||||
// Run "beforeAll" hooks, unless already run during previous tests.
|
// Run "beforeAll" hooks, unless already run during previous tests.
|
||||||
for (const suite of suites)
|
for (const suite of suites)
|
||||||
await this._runBeforeAllHooksForSuite(suite, testInfo);
|
await this._runBeforeAllHooksForSuite(suite, testInfo);
|
||||||
|
|
@ -376,13 +376,13 @@ export class WorkerMain extends ProcessRunner {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await testInfo._runAsStage({ title: 'test function', runnable: { type: 'test' } }, async () => {
|
await testInfo._runWithTimeout({ type: 'test' }, async () => {
|
||||||
// Now run the test itself.
|
// Now run the test itself.
|
||||||
const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
|
const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
|
||||||
await fn(testFunctionParams, testInfo);
|
await fn(testFunctionParams, testInfo);
|
||||||
checkForFloatingPromises('the test');
|
checkForFloatingPromises('the test');
|
||||||
});
|
});
|
||||||
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
})().catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
||||||
|
|
||||||
// Update duration, so it is available in fixture teardown and afterEach hooks.
|
// Update duration, so it is available in fixture teardown and afterEach hooks.
|
||||||
testInfo.duration = testInfo._timeoutManager.defaultSlot().elapsed | 0;
|
testInfo.duration = testInfo._timeoutManager.defaultSlot().elapsed | 0;
|
||||||
|
|
@ -396,12 +396,12 @@ export class WorkerMain extends ProcessRunner {
|
||||||
|
|
||||||
const FAILURE_AND_RECREATE_WORKER = testInfo._isFailure() && this._config.recreateWorkerAfterFailure;
|
const FAILURE_AND_RECREATE_WORKER = testInfo._isFailure() && this._config.recreateWorkerAfterFailure;
|
||||||
|
|
||||||
await testInfo._runAsStage({ title: 'After Hooks', stepInfo: { category: 'hook' } }, async () => {
|
await testInfo._runAsStep({ title: 'After Hooks', category: 'hook' }, async () => {
|
||||||
let firstAfterHooksError: Error | undefined;
|
let firstAfterHooksError: Error | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Run "immediately upon test function finish" callback.
|
// Run "immediately upon test function finish" callback.
|
||||||
await testInfo._runAsStage({ title: 'on-test-function-finish', runnable: { type: 'test', slot: afterHooksSlot } }, async () => testInfo._onDidFinishTestFunction?.());
|
await testInfo._runWithTimeout({ type: 'test', slot: afterHooksSlot }, async () => testInfo._onDidFinishTestFunction?.());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
firstAfterHooksError = firstAfterHooksError ?? error;
|
firstAfterHooksError = firstAfterHooksError ?? error;
|
||||||
}
|
}
|
||||||
|
|
@ -451,7 +451,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
// Mark as "cleaned up" early to avoid running cleanup twice.
|
// Mark as "cleaned up" early to avoid running cleanup twice.
|
||||||
this._didRunFullCleanup = true;
|
this._didRunFullCleanup = true;
|
||||||
|
|
||||||
await testInfo._runAsStage({ title: 'Worker Cleanup', stepInfo: { category: 'hook' } }, async () => {
|
await testInfo._runAsStep({ title: 'Worker Cleanup', category: 'hook' }, async () => {
|
||||||
let firstWorkerCleanupError: Error | undefined;
|
let firstWorkerCleanupError: Error | undefined;
|
||||||
|
|
||||||
// Give it more time for the full cleanup.
|
// Give it more time for the full cleanup.
|
||||||
|
|
@ -484,7 +484,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tracingSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
const tracingSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
||||||
await testInfo._runAsStage({ title: 'stop tracing', runnable: { type: 'test', slot: tracingSlot } }, async () => {
|
await testInfo._runWithTimeout({ type: 'test', slot: tracingSlot }, async () => {
|
||||||
await testInfo._tracing.stopIfNeeded();
|
await testInfo._tracing.stopIfNeeded();
|
||||||
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
||||||
|
|
||||||
|
|
@ -537,7 +537,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
let firstError: Error | undefined;
|
let firstError: Error | undefined;
|
||||||
for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) {
|
for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) {
|
||||||
try {
|
try {
|
||||||
await testInfo._runAsStage({ title: hook.title, stepInfo: { category: 'hook', location: hook.location } }, async () => {
|
await testInfo._runAsStep({ title: hook.title, category: 'hook', location: hook.location }, async () => {
|
||||||
// Separate time slot for each beforeAll/afterAll hook.
|
// Separate time slot for each beforeAll/afterAll hook.
|
||||||
const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
||||||
const runnable = { type: hook.type, slot: timeSlot, location: hook.location };
|
const runnable = { type: hook.type, slot: timeSlot, location: hook.location };
|
||||||
|
|
@ -590,7 +590,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await testInfo._runAsStage({ title: hook.title, stepInfo: { category: 'hook', location: hook.location } }, async () => {
|
await testInfo._runAsStep({ title: hook.title, category: 'hook', location: hook.location }, async () => {
|
||||||
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'test', runnable);
|
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'test', runnable);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
72
packages/playwright/types/test.d.ts
vendored
72
packages/playwright/types/test.d.ts
vendored
|
|
@ -884,7 +884,7 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* export default defineConfig({
|
* export default defineConfig({
|
||||||
* webServer: {
|
* webServer: {
|
||||||
* command: 'npm run start',
|
* command: 'npm run start',
|
||||||
* url: 'http://127.0.0.1:3000',
|
* url: 'http://localhost:3000',
|
||||||
* timeout: 120 * 1000,
|
* timeout: 120 * 1000,
|
||||||
* reuseExistingServer: !process.env.CI,
|
* reuseExistingServer: !process.env.CI,
|
||||||
* },
|
* },
|
||||||
|
|
@ -915,19 +915,19 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* webServer: [
|
* webServer: [
|
||||||
* {
|
* {
|
||||||
* command: 'npm run start',
|
* command: 'npm run start',
|
||||||
* url: 'http://127.0.0.1:3000',
|
* url: 'http://localhost:3000',
|
||||||
* timeout: 120 * 1000,
|
* timeout: 120 * 1000,
|
||||||
* reuseExistingServer: !process.env.CI,
|
* reuseExistingServer: !process.env.CI,
|
||||||
* },
|
* },
|
||||||
* {
|
* {
|
||||||
* command: 'npm run backend',
|
* command: 'npm run backend',
|
||||||
* url: 'http://127.0.0.1:3333',
|
* url: 'http://localhost:3333',
|
||||||
* timeout: 120 * 1000,
|
* timeout: 120 * 1000,
|
||||||
* reuseExistingServer: !process.env.CI,
|
* reuseExistingServer: !process.env.CI,
|
||||||
* }
|
* }
|
||||||
* ],
|
* ],
|
||||||
* use: {
|
* use: {
|
||||||
* baseURL: 'http://127.0.0.1:3000',
|
* baseURL: 'http://localhost:3000',
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
|
|
@ -1284,10 +1284,11 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
/**
|
/**
|
||||||
* Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as
|
* Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as
|
||||||
* key-value pairs, and JSON report will include metadata serialized as json.
|
* key-value pairs, and JSON report will include metadata serialized as json.
|
||||||
|
* - Providing `gitCommit: 'generate'` property will populate it with the git commit details.
|
||||||
|
* - Providing `gitDiff: 'generate'` property will populate it with the git diff details.
|
||||||
*
|
*
|
||||||
* See also
|
* On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic
|
||||||
* [testConfig.populateGitInfo](https://playwright.dev/docs/api/class-testconfig#test-config-populate-git-info) that
|
* generation.
|
||||||
* populates metadata.
|
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
|
|
@ -1360,29 +1361,6 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
*/
|
*/
|
||||||
outputDir?: string;
|
outputDir?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to populate `'git.commit.info'` field of the
|
|
||||||
* [testConfig.metadata](https://playwright.dev/docs/api/class-testconfig#test-config-metadata) with Git commit info
|
|
||||||
* and CI/CD information.
|
|
||||||
*
|
|
||||||
* This information will appear in the HTML and JSON reports and is available in the Reporter API.
|
|
||||||
*
|
|
||||||
* On Github Actions, this feature is enabled by default.
|
|
||||||
*
|
|
||||||
* **Usage**
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* // playwright.config.ts
|
|
||||||
* import { defineConfig } from '@playwright/test';
|
|
||||||
*
|
|
||||||
* export default defineConfig({
|
|
||||||
* populateGitInfo: !!process.env.CI,
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
populateGitInfo?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to preserve test output in the
|
* Whether to preserve test output in the
|
||||||
* [testConfig.outputDir](https://playwright.dev/docs/api/class-testconfig#test-config-output-dir). Defaults to
|
* [testConfig.outputDir](https://playwright.dev/docs/api/class-testconfig#test-config-output-dir). Defaults to
|
||||||
|
|
@ -1467,7 +1445,7 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
max: number;
|
max: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test duration in milliseconds that is considered slow. Defaults to 15 seconds.
|
* Test file duration in milliseconds that is considered slow. Defaults to 5 minutes.
|
||||||
*/
|
*/
|
||||||
threshold: number;
|
threshold: number;
|
||||||
};
|
};
|
||||||
|
|
@ -1919,12 +1897,12 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
*/
|
*/
|
||||||
reportSlowTests: null|{
|
reportSlowTests: null|{
|
||||||
/**
|
/**
|
||||||
* The maximum number of slow test files to report. Defaults to `5`.
|
* The maximum number of slow test files to report.
|
||||||
*/
|
*/
|
||||||
max: number;
|
max: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test duration in milliseconds that is considered slow. Defaults to 15 seconds.
|
* Test file duration in milliseconds that is considered slow.
|
||||||
*/
|
*/
|
||||||
threshold: number;
|
threshold: number;
|
||||||
};
|
};
|
||||||
|
|
@ -8817,14 +8795,14 @@ interface LocatorAssertions {
|
||||||
/**
|
/**
|
||||||
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
|
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
|
||||||
*
|
*
|
||||||
* Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate`
|
* Snapshot is stored in a separate `.snapshot.yml` file in a location configured by
|
||||||
* and/or `snapshotPathTemplate` properties in the configuration file.
|
* `expect.toMatchAriaSnapshot.pathTemplate` and/or `snapshotPathTemplate` properties in the configuration file.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* await expect(page.locator('body')).toMatchAriaSnapshot();
|
* await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||||
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
|
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.snapshot.yml' });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param options
|
* @param options
|
||||||
|
|
@ -8926,13 +8904,25 @@ interface PageAssertions {
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* await expect(page).toHaveURL(/.*checkout/);
|
* // Check for the page URL to be 'https://playwright.dev/docs/intro' (including query string)
|
||||||
|
* await expect(page).toHaveURL('https://playwright.dev/docs/intro');
|
||||||
|
*
|
||||||
|
* // Check for the page URL to contain 'doc', followed by an optional 's', followed by '/'
|
||||||
|
* await expect(page).toHaveURL(/docs?\//);
|
||||||
|
*
|
||||||
|
* // Check for the predicate to be satisfied
|
||||||
|
* // For example: verify query strings
|
||||||
|
* await expect(page).toHaveURL(url => {
|
||||||
|
* const params = url.searchParams;
|
||||||
|
* return params.has('search') && params.has('options') && params.get('id') === '5';
|
||||||
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param url Expected URL string, RegExp, or predicate receiving [URL] to match. When a
|
* @param url Expected URL string, RegExp, or predicate receiving [URL] to match. When
|
||||||
* [`baseURL`](https://playwright.dev/docs/api/class-browser#browser-new-context-option-base-url) via the context
|
* [`baseURL`](https://playwright.dev/docs/api/class-browser#browser-new-context-option-base-url) is provided via the
|
||||||
* options was provided and the passed URL is a path, it gets merged via the
|
* context options and the `url` argument is a string, the two values are merged via the
|
||||||
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor and used for the comparison
|
||||||
|
* against the current browser URL.
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
toHaveURL(url: string|RegExp|((url: URL) => boolean), options?: {
|
toHaveURL(url: string|RegExp|((url: URL) => boolean), options?: {
|
||||||
|
|
|
||||||
12
packages/protocol/src/channels.d.ts
vendored
12
packages/protocol/src/channels.d.ts
vendored
|
|
@ -623,7 +623,7 @@ export type PlaywrightNewRequestParams = {
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
clientCertificates?: {
|
clientCertificates?: {
|
||||||
origin: string,
|
origin: string,
|
||||||
cert?: Binary,
|
cert?: Binary,
|
||||||
|
|
@ -655,7 +655,7 @@ export type PlaywrightNewRequestOptions = {
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
clientCertificates?: {
|
clientCertificates?: {
|
||||||
origin: string,
|
origin: string,
|
||||||
cert?: Binary,
|
cert?: Binary,
|
||||||
|
|
@ -1029,7 +1029,6 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
||||||
},
|
},
|
||||||
permissions?: string[],
|
permissions?: string[],
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
|
||||||
offline?: boolean,
|
offline?: boolean,
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
|
|
@ -1111,7 +1110,6 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
||||||
},
|
},
|
||||||
permissions?: string[],
|
permissions?: string[],
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
|
||||||
offline?: boolean,
|
offline?: boolean,
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
|
|
@ -1228,7 +1226,6 @@ export type BrowserNewContextParams = {
|
||||||
},
|
},
|
||||||
permissions?: string[],
|
permissions?: string[],
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
|
||||||
offline?: boolean,
|
offline?: boolean,
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
|
|
@ -1296,7 +1293,6 @@ export type BrowserNewContextOptions = {
|
||||||
},
|
},
|
||||||
permissions?: string[],
|
permissions?: string[],
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
|
||||||
offline?: boolean,
|
offline?: boolean,
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
|
|
@ -1367,7 +1363,6 @@ export type BrowserNewContextForReuseParams = {
|
||||||
},
|
},
|
||||||
permissions?: string[],
|
permissions?: string[],
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
|
||||||
offline?: boolean,
|
offline?: boolean,
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
|
|
@ -1435,7 +1430,6 @@ export type BrowserNewContextForReuseOptions = {
|
||||||
},
|
},
|
||||||
permissions?: string[],
|
permissions?: string[],
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
|
||||||
offline?: boolean,
|
offline?: boolean,
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
|
|
@ -4802,7 +4796,6 @@ export type AndroidDeviceLaunchBrowserParams = {
|
||||||
},
|
},
|
||||||
permissions?: string[],
|
permissions?: string[],
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
|
||||||
offline?: boolean,
|
offline?: boolean,
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
|
|
@ -4868,7 +4861,6 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
||||||
},
|
},
|
||||||
permissions?: string[],
|
permissions?: string[],
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
apiRequestFailsOnErrorStatus?: boolean,
|
|
||||||
offline?: boolean,
|
offline?: boolean,
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
|
|
|
||||||
|
|
@ -520,7 +520,6 @@ ContextOptions:
|
||||||
extraHTTPHeaders:
|
extraHTTPHeaders:
|
||||||
type: array?
|
type: array?
|
||||||
items: NameValue
|
items: NameValue
|
||||||
apiRequestFailsOnErrorStatus: boolean?
|
|
||||||
offline: boolean?
|
offline: boolean?
|
||||||
httpCredentials:
|
httpCredentials:
|
||||||
type: object?
|
type: object?
|
||||||
|
|
@ -752,7 +751,7 @@ Playwright:
|
||||||
extraHTTPHeaders:
|
extraHTTPHeaders:
|
||||||
type: array?
|
type: array?
|
||||||
items: NameValue
|
items: NameValue
|
||||||
apiRequestFailsOnErrorStatus: boolean?
|
failOnStatusCode: boolean?
|
||||||
clientCertificates:
|
clientCertificates:
|
||||||
type: array?
|
type: array?
|
||||||
items:
|
items:
|
||||||
|
|
|
||||||
|
|
@ -24,20 +24,21 @@ import type { StackFrame } from '@protocol/channels';
|
||||||
import { CopyToClipboardTextButton } from './copyToClipboard';
|
import { CopyToClipboardTextButton } from './copyToClipboard';
|
||||||
import { attachmentURL } from './attachmentsTab';
|
import { attachmentURL } from './attachmentsTab';
|
||||||
import { fixTestPrompt } from '@web/components/prompts';
|
import { fixTestPrompt } from '@web/components/prompts';
|
||||||
import type { GitCommitInfo } from '@testIsomorphic/types';
|
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||||
import { AIConversation } from './aiConversation';
|
import { AIConversation } from './aiConversation';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { useIsLLMAvailable, useLLMChat } from './llm';
|
import { useIsLLMAvailable, useLLMChat } from './llm';
|
||||||
import { useAsyncMemo } from '@web/uiUtils';
|
import { useAsyncMemo } from '@web/uiUtils';
|
||||||
|
import { useSources } from './sourceTab';
|
||||||
|
|
||||||
const GitCommitInfoContext = React.createContext<GitCommitInfo | undefined>(undefined);
|
const CommitInfoContext = React.createContext<MetadataWithCommitInfo | undefined>(undefined);
|
||||||
|
|
||||||
export function GitCommitInfoProvider({ children, gitCommitInfo }: React.PropsWithChildren<{ gitCommitInfo: GitCommitInfo }>) {
|
export function CommitInfoProvider({ children, commitInfo }: React.PropsWithChildren<{ commitInfo: MetadataWithCommitInfo }>) {
|
||||||
return <GitCommitInfoContext.Provider value={gitCommitInfo}>{children}</GitCommitInfoContext.Provider>;
|
return <CommitInfoContext.Provider value={commitInfo}>{children}</CommitInfoContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGitCommitInfo() {
|
export function useCommitInfo() {
|
||||||
return React.useContext(GitCommitInfoContext);
|
return React.useContext(CommitInfoContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePageSnapshot(actions: modelUtil.ActionTraceEventInContext[]) {
|
function usePageSnapshot(actions: modelUtil.ActionTraceEventInContext[]) {
|
||||||
|
|
@ -53,18 +54,47 @@ function usePageSnapshot(actions: modelUtil.ActionTraceEventInContext[]) {
|
||||||
}, [actions], undefined);
|
}, [actions], undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useCodeFrame(stack: StackFrame[] | undefined, sources: Map<string, modelUtil.SourceModel>, width: number) {
|
||||||
|
const selectedFrame = stack?.[0];
|
||||||
|
const { source } = useSources(stack, 0, sources);
|
||||||
|
return React.useMemo(() => {
|
||||||
|
if (!source.content)
|
||||||
|
return '';
|
||||||
|
|
||||||
|
const targetLine = selectedFrame?.line ?? 0;
|
||||||
|
|
||||||
|
const lines = source.content.split('\n');
|
||||||
|
const start = Math.max(0, targetLine - width);
|
||||||
|
const end = Math.min(lines.length, targetLine + width);
|
||||||
|
const lineNumberWidth = String(end).length;
|
||||||
|
const codeFrame = lines.slice(start, end).map((line, i) => {
|
||||||
|
const lineNumber = start + i + 1;
|
||||||
|
const paddedLineNumber = String(lineNumber).padStart(lineNumberWidth, ' ');
|
||||||
|
if (lineNumber !== targetLine)
|
||||||
|
return ` ${(paddedLineNumber)} | ${line}`;
|
||||||
|
|
||||||
|
let highlightLine = `> ${paddedLineNumber} | ${line}`;
|
||||||
|
if (selectedFrame?.column)
|
||||||
|
highlightLine += `\n${' '.repeat(4 + lineNumberWidth + selectedFrame.column)}^`;
|
||||||
|
return highlightLine;
|
||||||
|
}).join('\n');
|
||||||
|
return codeFrame;
|
||||||
|
}, [source, selectedFrame, width]);
|
||||||
|
}
|
||||||
|
|
||||||
const CopyPromptButton: React.FC<{
|
const CopyPromptButton: React.FC<{
|
||||||
error: string;
|
error: string;
|
||||||
|
codeFrame: string;
|
||||||
pageSnapshot?: string;
|
pageSnapshot?: string;
|
||||||
diff?: string;
|
diff?: string;
|
||||||
}> = ({ error, pageSnapshot, diff }) => {
|
}> = ({ error, codeFrame, pageSnapshot, diff }) => {
|
||||||
const prompt = React.useMemo(
|
const prompt = React.useMemo(
|
||||||
() => fixTestPrompt(
|
() => fixTestPrompt(
|
||||||
error,
|
error + '\n\n' + codeFrame,
|
||||||
diff,
|
diff,
|
||||||
pageSnapshot
|
pageSnapshot
|
||||||
),
|
),
|
||||||
[error, diff, pageSnapshot]
|
[error, diff, codeFrame, pageSnapshot]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -72,7 +102,7 @@ const CopyPromptButton: React.FC<{
|
||||||
value={prompt}
|
value={prompt}
|
||||||
description='Copy as Prompt'
|
description='Copy as Prompt'
|
||||||
copiedDescription={<>Copied <span className='codicon codicon-copy' style={{ marginLeft: '5px' }}/></>}
|
copiedDescription={<>Copied <span className='codicon codicon-copy' style={{ marginLeft: '5px' }}/></>}
|
||||||
style={{ width: '90px', justifyContent: 'center' }}
|
style={{ width: '120px', justifyContent: 'center' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -97,11 +127,10 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined):
|
||||||
}, [model]);
|
}, [model]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSource }: { message: string, error: ErrorDescription, errorId: string, sdkLanguage: Language, pageSnapshot?: string, revealInSource: (error: ErrorDescription) => void }) {
|
function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSource, sources }: { message: string, error: ErrorDescription, errorId: string, sdkLanguage: Language, pageSnapshot?: string, revealInSource: (error: ErrorDescription) => void, sources: Map<string, modelUtil.SourceModel> }) {
|
||||||
const [showLLM, setShowLLM] = React.useState(false);
|
const [showLLM, setShowLLM] = React.useState(false);
|
||||||
const llmAvailable = useIsLLMAvailable();
|
const llmAvailable = useIsLLMAvailable();
|
||||||
const gitCommitInfo = useGitCommitInfo();
|
const metadata = useCommitInfo();
|
||||||
const diff = gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'];
|
|
||||||
|
|
||||||
let location: string | undefined;
|
let location: string | undefined;
|
||||||
let longLocation: string | undefined;
|
let longLocation: string | undefined;
|
||||||
|
|
@ -112,6 +141,8 @@ function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSou
|
||||||
longLocation = stackFrame.file + ':' + stackFrame.line;
|
longLocation = stackFrame.file + ':' + stackFrame.line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codeFrame = useCodeFrame(error.stack, sources, 3);
|
||||||
|
|
||||||
return <div style={{ display: 'flex', flexDirection: 'column', overflowX: 'clip' }}>
|
return <div style={{ display: 'flex', flexDirection: 'column', overflowX: 'clip' }}>
|
||||||
<div className='hbox' style={{
|
<div className='hbox' style={{
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
@ -127,8 +158,8 @@ function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSou
|
||||||
</div>}
|
</div>}
|
||||||
<span style={{ position: 'absolute', right: '5px' }}>
|
<span style={{ position: 'absolute', right: '5px' }}>
|
||||||
{llmAvailable
|
{llmAvailable
|
||||||
? <FixWithAIButton conversationId={errorId} onChange={setShowLLM} value={showLLM} error={message} diff={diff} pageSnapshot={pageSnapshot} />
|
? <FixWithAIButton conversationId={errorId} onChange={setShowLLM} value={showLLM} error={message} diff={metadata?.gitDiff} pageSnapshot={pageSnapshot} />
|
||||||
: <CopyPromptButton error={message} pageSnapshot={pageSnapshot} diff={diff} />}
|
: <CopyPromptButton error={message} codeFrame={codeFrame} pageSnapshot={pageSnapshot} diff={metadata?.gitDiff} />}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -180,9 +211,10 @@ export const ErrorsTab: React.FunctionComponent<{
|
||||||
errorsModel: ErrorsTabModel,
|
errorsModel: ErrorsTabModel,
|
||||||
actions: modelUtil.ActionTraceEventInContext[],
|
actions: modelUtil.ActionTraceEventInContext[],
|
||||||
wallTime: number,
|
wallTime: number,
|
||||||
|
sources: Map<string, modelUtil.SourceModel>,
|
||||||
sdkLanguage: Language,
|
sdkLanguage: Language,
|
||||||
revealInSource: (error: ErrorDescription) => void,
|
revealInSource: (error: ErrorDescription) => void,
|
||||||
}> = ({ errorsModel, sdkLanguage, revealInSource, actions, wallTime }) => {
|
}> = ({ errorsModel, sdkLanguage, revealInSource, actions, wallTime, sources }) => {
|
||||||
const pageSnapshot = usePageSnapshot(actions);
|
const pageSnapshot = usePageSnapshot(actions);
|
||||||
|
|
||||||
if (!errorsModel.errors.size)
|
if (!errorsModel.errors.size)
|
||||||
|
|
@ -191,7 +223,7 @@ export const ErrorsTab: React.FunctionComponent<{
|
||||||
return <div className='fill' style={{ overflow: 'auto' }}>
|
return <div className='fill' style={{ overflow: 'auto' }}>
|
||||||
{[...errorsModel.errors.entries()].map(([message, error]) => {
|
{[...errorsModel.errors.entries()].map(([message, error]) => {
|
||||||
const errorId = `error-${wallTime}-${message}`;
|
const errorId = `error-${wallTime}-${message}`;
|
||||||
return <Error key={errorId} errorId={errorId} message={message} error={error} revealInSource={revealInSource} sdkLanguage={sdkLanguage} pageSnapshot={pageSnapshot} />;
|
return <Error key={errorId} errorId={errorId} message={message} error={error} sources={sources} revealInSource={revealInSource} sdkLanguage={sdkLanguage} pageSnapshot={pageSnapshot} />;
|
||||||
})}
|
})}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -27,25 +27,8 @@ import { CopyToClipboard } from './copyToClipboard';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { Toolbar } from '@web/components/toolbar';
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
|
|
||||||
export const SourceTab: React.FunctionComponent<{
|
export function useSources(stack: StackFrame[] | undefined, selectedFrame: number, sources: Map<string, SourceModel>, rootDir?: string, fallbackLocation?: SourceLocation) {
|
||||||
stack?: StackFrame[],
|
return useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[], location?: SourceLocation }>(async () => {
|
||||||
stackFrameLocation: 'bottom' | 'right',
|
|
||||||
sources: Map<string, SourceModel>,
|
|
||||||
rootDir?: string,
|
|
||||||
fallbackLocation?: SourceLocation,
|
|
||||||
onOpenExternally?: (location: SourceLocation) => void,
|
|
||||||
}> = ({ stack, sources, rootDir, fallbackLocation, stackFrameLocation, onOpenExternally }) => {
|
|
||||||
const [lastStack, setLastStack] = React.useState<StackFrame[] | undefined>();
|
|
||||||
const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (lastStack !== stack) {
|
|
||||||
setLastStack(stack);
|
|
||||||
setSelectedFrame(0);
|
|
||||||
}
|
|
||||||
}, [stack, lastStack, setLastStack, setSelectedFrame]);
|
|
||||||
|
|
||||||
const { source, highlight, targetLine, fileName, location } = useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[], location?: SourceLocation }>(async () => {
|
|
||||||
const actionLocation = stack?.[selectedFrame];
|
const actionLocation = stack?.[selectedFrame];
|
||||||
const shouldUseFallback = !actionLocation?.file;
|
const shouldUseFallback = !actionLocation?.file;
|
||||||
if (shouldUseFallback && !fallbackLocation)
|
if (shouldUseFallback && !fallbackLocation)
|
||||||
|
|
@ -84,6 +67,27 @@ export const SourceTab: React.FunctionComponent<{
|
||||||
}
|
}
|
||||||
return { source, highlight, targetLine, fileName, location };
|
return { source, highlight, targetLine, fileName, location };
|
||||||
}, [stack, selectedFrame, rootDir, fallbackLocation], { source: { errors: [], content: 'Loading\u2026' }, highlight: [] });
|
}, [stack, selectedFrame, rootDir, fallbackLocation], { source: { errors: [], content: 'Loading\u2026' }, highlight: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SourceTab: React.FunctionComponent<{
|
||||||
|
stack?: StackFrame[],
|
||||||
|
stackFrameLocation: 'bottom' | 'right',
|
||||||
|
sources: Map<string, SourceModel>,
|
||||||
|
rootDir?: string,
|
||||||
|
fallbackLocation?: SourceLocation,
|
||||||
|
onOpenExternally?: (location: SourceLocation) => void,
|
||||||
|
}> = ({ stack, sources, rootDir, fallbackLocation, stackFrameLocation, onOpenExternally }) => {
|
||||||
|
const [lastStack, setLastStack] = React.useState<StackFrame[] | undefined>();
|
||||||
|
const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (lastStack !== stack) {
|
||||||
|
setLastStack(stack);
|
||||||
|
setSelectedFrame(0);
|
||||||
|
}
|
||||||
|
}, [stack, lastStack, setLastStack, setSelectedFrame]);
|
||||||
|
|
||||||
|
const { source, highlight, targetLine, fileName, location } = useSources(stack, selectedFrame, sources, rootDir, fallbackLocation);
|
||||||
|
|
||||||
const openExternally = React.useCallback(() => {
|
const openExternally = React.useCallback(() => {
|
||||||
if (!location)
|
if (!location)
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,9 @@ import { TestListView } from './uiModeTestListView';
|
||||||
import { TraceView } from './uiModeTraceView';
|
import { TraceView } from './uiModeTraceView';
|
||||||
import { SettingsView } from './settingsView';
|
import { SettingsView } from './settingsView';
|
||||||
import { DefaultSettingsView } from './defaultSettingsView';
|
import { DefaultSettingsView } from './defaultSettingsView';
|
||||||
import { GitCommitInfoProvider } from './errorsTab';
|
import { CommitInfoProvider } from './errorsTab';
|
||||||
import { LLMProvider } from './llm';
|
import { LLMProvider } from './llm';
|
||||||
|
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||||
|
|
||||||
let xtermSize = { cols: 80, rows: 24 };
|
let xtermSize = { cols: 80, rows: 24 };
|
||||||
const xtermDataSource: XtermDataSource = {
|
const xtermDataSource: XtermDataSource = {
|
||||||
|
|
@ -432,7 +433,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx('vbox', isShowingOutput && 'hidden')}>
|
<div className={clsx('vbox', isShowingOutput && 'hidden')}>
|
||||||
<GitCommitInfoProvider gitCommitInfo={testModel?.config.metadata['git.commit.info']}>
|
<CommitInfoProvider commitInfo={testModel?.config.metadata as MetadataWithCommitInfo}>
|
||||||
<TraceView
|
<TraceView
|
||||||
pathSeparator={queryParams.pathSeparator}
|
pathSeparator={queryParams.pathSeparator}
|
||||||
item={selectedItem}
|
item={selectedItem}
|
||||||
|
|
@ -440,7 +441,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
revealSource={revealSource}
|
revealSource={revealSource}
|
||||||
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
|
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
|
||||||
/>
|
/>
|
||||||
</GitCommitInfoProvider>
|
</CommitInfoProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
sidebar={<div className='vbox ui-mode-sidebar'>
|
sidebar={<div className='vbox ui-mode-sidebar'>
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
id: 'errors',
|
id: 'errors',
|
||||||
title: 'Errors',
|
title: 'Errors',
|
||||||
errorCount: errorsModel.errors.size,
|
errorCount: errorsModel.errors.size,
|
||||||
render: () => <ErrorsTab errorsModel={errorsModel} sdkLanguage={sdkLanguage} revealInSource={error => {
|
render: () => <ErrorsTab errorsModel={errorsModel} sources={sources} sdkLanguage={sdkLanguage} revealInSource={error => {
|
||||||
if (error.action)
|
if (error.action)
|
||||||
setSelectedAction(error.action);
|
setSelectedAction(error.action);
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { browserTest as it, expect } from '../config/browserTest';
|
|
||||||
|
|
||||||
it('should throw when apiRequestFailsOnErrorStatus is set to true inside BrowserContext options', async ({ browser, server }) => {
|
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' });
|
|
||||||
const context = await browser.newContext({ apiRequestFailsOnErrorStatus: true });
|
|
||||||
server.setRoute('/empty.html', (req, res) => {
|
|
||||||
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Not found.');
|
|
||||||
});
|
|
||||||
const error = await context.request.fetch(server.EMPTY_PAGE).catch(e => e);
|
|
||||||
expect(error.message).toContain('404 Not Found');
|
|
||||||
await context.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not throw when failOnStatusCode is set to false inside BrowserContext options', async ({ browser, server }) => {
|
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' });
|
|
||||||
const context = await browser.newContext({ apiRequestFailsOnErrorStatus: false });
|
|
||||||
server.setRoute('/empty.html', (req, res) => {
|
|
||||||
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Not found.');
|
|
||||||
});
|
|
||||||
const error = await context.request.fetch(server.EMPTY_PAGE).catch(e => e);
|
|
||||||
expect(error.message).toBeUndefined();
|
|
||||||
await context.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw when apiRequestFailsOnErrorStatus is set to true inside browserType.launchPersistentContext options', async ({ browserType, server, createUserDataDir }) => {
|
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' });
|
|
||||||
const userDataDir = await createUserDataDir();
|
|
||||||
const context = await browserType.launchPersistentContext(userDataDir, { apiRequestFailsOnErrorStatus: true });
|
|
||||||
server.setRoute('/empty.html', (req, res) => {
|
|
||||||
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Not found.');
|
|
||||||
});
|
|
||||||
const error = await context.request.fetch(server.EMPTY_PAGE).catch(e => e);
|
|
||||||
expect(error.message).toContain('404 Not Found');
|
|
||||||
await context.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not throw when apiRequestFailsOnErrorStatus is set to false inside browserType.launchPersistentContext options', async ({ browserType, server, createUserDataDir }) => {
|
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' });
|
|
||||||
const userDataDir = await createUserDataDir();
|
|
||||||
const context = await browserType.launchPersistentContext(userDataDir, { apiRequestFailsOnErrorStatus: false });
|
|
||||||
server.setRoute('/empty.html', (req, res) => {
|
|
||||||
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Not found.');
|
|
||||||
});
|
|
||||||
const error = await context.request.fetch(server.EMPTY_PAGE).catch(e => e);
|
|
||||||
expect(error.message).toBeUndefined();
|
|
||||||
await context.close();
|
|
||||||
});
|
|
||||||
|
|
@ -110,7 +110,7 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
const path = testInfo.outputPath('storage-state.json');
|
const path = testInfo.outputPath('storage-state.json');
|
||||||
const state = await context.storageState({ path });
|
const state = await context.storageState({ path, indexedDB: true });
|
||||||
const written = await fs.promises.readFile(path, 'utf8');
|
const written = await fs.promises.readFile(path, 'utf8');
|
||||||
expect(JSON.stringify(state, undefined, 2)).toBe(written);
|
expect(JSON.stringify(state, undefined, 2)).toBe(written);
|
||||||
|
|
||||||
|
|
@ -365,7 +365,7 @@ it('should support IndexedDB', async ({ page, server, contextFactory }) => {
|
||||||
await page.getByLabel('Mins').fill('1');
|
await page.getByLabel('Mins').fill('1');
|
||||||
await page.getByText('Add Task').click();
|
await page.getByText('Add Task').click();
|
||||||
|
|
||||||
const storageState = await page.context().storageState();
|
const storageState = await page.context().storageState({ indexedDB: true });
|
||||||
expect(storageState.origins).toEqual([
|
expect(storageState.origins).toEqual([
|
||||||
{
|
{
|
||||||
origin: server.PREFIX,
|
origin: server.PREFIX,
|
||||||
|
|
@ -438,7 +438,7 @@ it('should support IndexedDB', async ({ page, server, contextFactory }) => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const context = await contextFactory({ storageState });
|
const context = await contextFactory({ storageState });
|
||||||
expect(await context.storageState()).toEqual(storageState);
|
expect(await context.storageState({ indexedDB: true })).toEqual(storageState);
|
||||||
|
|
||||||
const recreatedPage = await context.newPage();
|
const recreatedPage = await context.newPage();
|
||||||
await recreatedPage.goto(server.PREFIX + '/to-do-notifications/index.html');
|
await recreatedPage.goto(server.PREFIX + '/to-do-notifications/index.html');
|
||||||
|
|
@ -448,5 +448,5 @@ it('should support IndexedDB', async ({ page, server, contextFactory }) => {
|
||||||
- text: /Pet the cat/
|
- text: /Pet the cat/
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(await context.storageState({ indexedDB: false })).toEqual({ cookies: [], origins: [] });
|
expect(await context.storageState()).toEqual({ cookies: [], origins: [] });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -376,7 +376,7 @@ it('should preserve local storage on import/export of storage state', async ({ p
|
||||||
};
|
};
|
||||||
const request = await playwright.request.newContext({ storageState });
|
const request = await playwright.request.newContext({ storageState });
|
||||||
await request.get(server.EMPTY_PAGE);
|
await request.get(server.EMPTY_PAGE);
|
||||||
const exportedState = await request.storageState();
|
const exportedState = await request.storageState({ indexedDB: true });
|
||||||
expect(exportedState).toEqual(storageState);
|
expect(exportedState).toEqual(storageState);
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -537,9 +537,9 @@ it('should retry ECONNRESET', {
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when apiRequestFailsOnErrorStatus is set to true inside APIRequest context options', async ({ playwright, server }) => {
|
it('should throw when failOnStatusCode is set to true inside APIRequest context options', async ({ playwright, server }) => {
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' });
|
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' });
|
||||||
const request = await playwright.request.newContext({ apiRequestFailsOnErrorStatus: true });
|
const request = await playwright.request.newContext({ failOnStatusCode: true });
|
||||||
server.setRoute('/empty.html', (req, res) => {
|
server.setRoute('/empty.html', (req, res) => {
|
||||||
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
||||||
res.end('Not found.');
|
res.end('Not found.');
|
||||||
|
|
@ -549,9 +549,9 @@ it('should throw when apiRequestFailsOnErrorStatus is set to true inside APIRequ
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw when apiRequestFailsOnErrorStatus is set to false inside APIRequest context options', async ({ playwright, server }) => {
|
it('should not throw when failOnStatusCode is set to false inside APIRequest context options', async ({ playwright, server }) => {
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' });
|
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' });
|
||||||
const request = await playwright.request.newContext({ apiRequestFailsOnErrorStatus: false });
|
const request = await playwright.request.newContext({ failOnStatusCode: false });
|
||||||
server.setRoute('/empty.html', (req, res) => {
|
server.setRoute('/empty.html', (req, res) => {
|
||||||
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
||||||
res.end('Not found.');
|
res.end('Not found.');
|
||||||
|
|
|
||||||
|
|
@ -92,13 +92,14 @@ it('should support locator.or()', async ({ page }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support playwright.getBy*', async ({ page }) => {
|
it('should support playwright.getBy*', async ({ page }) => {
|
||||||
await page.setContent('<span>Hello</span><span title="world">World</span>');
|
await page.setContent('<span>Hello</span><span title="world">World</span><div>one</div><div style="display:none">two</div>');
|
||||||
expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello');
|
expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello');
|
||||||
expect(await page.evaluate(`playwright.getByTitle('world').element.innerHTML`)).toContain('World');
|
expect(await page.evaluate(`playwright.getByTitle('world').element.innerHTML`)).toContain('World');
|
||||||
expect(await page.evaluate(`playwright.locator('span').filter({ hasText: 'hello' }).element.innerHTML`)).toContain('Hello');
|
expect(await page.evaluate(`playwright.locator('span').filter({ hasText: 'hello' }).element.innerHTML`)).toContain('Hello');
|
||||||
expect(await page.evaluate(`playwright.locator('span').first().element.innerHTML`)).toContain('Hello');
|
expect(await page.evaluate(`playwright.locator('span').first().element.innerHTML`)).toContain('Hello');
|
||||||
expect(await page.evaluate(`playwright.locator('span').last().element.innerHTML`)).toContain('World');
|
expect(await page.evaluate(`playwright.locator('span').last().element.innerHTML`)).toContain('World');
|
||||||
expect(await page.evaluate(`playwright.locator('span').nth(1).element.innerHTML`)).toContain('World');
|
expect(await page.evaluate(`playwright.locator('span').nth(1).element.innerHTML`)).toContain('World');
|
||||||
|
expect(await page.evaluate(`playwright.locator('div').filter({ visible: false }).element.innerHTML`)).toContain('two');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('expected properties on playwright object', async ({ page }) => {
|
it('expected properties on playwright object', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,21 @@ it('reverse engineer hasNotText', async ({ page }) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reverse engineer visible', async ({ page }) => {
|
||||||
|
expect.soft(generate(page.getByText('Hello').filter({ visible: true }).locator('div'))).toEqual({
|
||||||
|
csharp: `GetByText("Hello").Filter(new() { Visible = true }).Locator("div")`,
|
||||||
|
java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(true)).locator("div")`,
|
||||||
|
javascript: `getByText('Hello').filter({ visible: true }).locator('div')`,
|
||||||
|
python: `get_by_text("Hello").filter(visible=True).locator("div")`,
|
||||||
|
});
|
||||||
|
expect.soft(generate(page.getByText('Hello').filter({ visible: false }).locator('div'))).toEqual({
|
||||||
|
csharp: `GetByText("Hello").Filter(new() { Visible = false }).Locator("div")`,
|
||||||
|
java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(false)).locator("div")`,
|
||||||
|
javascript: `getByText('Hello').filter({ visible: false }).locator('div')`,
|
||||||
|
python: `get_by_text("Hello").filter(visible=False).locator("div")`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('reverse engineer has', async ({ page }) => {
|
it('reverse engineer has', async ({ page }) => {
|
||||||
expect.soft(generate(page.getByText('Hello').filter({ has: page.locator('div').getByText('bye') }))).toEqual({
|
expect.soft(generate(page.getByText('Hello').filter({ has: page.locator('div').getByText('bye') }))).toEqual({
|
||||||
csharp: `GetByText("Hello").Filter(new() { Has = Locator("div").GetByText("bye") })`,
|
csharp: `GetByText("Hello").Filter(new() { Has = Locator("div").GetByText("bye") })`,
|
||||||
|
|
|
||||||
|
|
@ -372,6 +372,13 @@ test('display:contents should be visible when contents are visible', async ({ pa
|
||||||
await expect(page.getByRole('button')).toHaveCount(1);
|
await expect(page.getByRole('button')).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should remove soft hyphens and zero-width spaces', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<button>1\u00ad2\u200b3</button>
|
||||||
|
`);
|
||||||
|
expect.soft(await getNameAndRole(page, 'button')).toEqual({ role: 'button', name: '123' });
|
||||||
|
});
|
||||||
|
|
||||||
test('label/labelled-by aria-hidden with descendants', async ({ page }) => {
|
test('label/labelled-by aria-hidden with descendants', async ({ page }) => {
|
||||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29796' });
|
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29796' });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ test.describe('toHaveText with text', () => {
|
||||||
await expect(locator).toHaveText('text CONTENT', { ignoreCase: true });
|
await expect(locator).toHaveText('text CONTENT', { ignoreCase: true });
|
||||||
// Should support falsy ignoreCase.
|
// Should support falsy ignoreCase.
|
||||||
await expect(locator).not.toHaveText('TEXT', { ignoreCase: false });
|
await expect(locator).not.toHaveText('TEXT', { ignoreCase: false });
|
||||||
|
// Should normalize soft hyphens.
|
||||||
|
await expect(locator).toHaveText('T\u00ade\u00adxt content');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pass contain', async ({ page }) => {
|
test('pass contain', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,14 @@ it('should work with glob', async () => {
|
||||||
expect(globToRegex('http://localhost:3000/signin-oidc*').test('http://localhost:3000/signin-oidc/foo')).toBeFalsy();
|
expect(globToRegex('http://localhost:3000/signin-oidc*').test('http://localhost:3000/signin-oidc/foo')).toBeFalsy();
|
||||||
expect(globToRegex('http://localhost:3000/signin-oidc*').test('http://localhost:3000/signin-oidcnice')).toBeTruthy();
|
expect(globToRegex('http://localhost:3000/signin-oidc*').test('http://localhost:3000/signin-oidcnice')).toBeTruthy();
|
||||||
|
|
||||||
expect(globToRegex('**/three-columns/settings.html?**id=[a-z]**').test('http://mydomain:8080/blah/blah/three-columns/settings.html?id=settings-e3c58efe-02e9-44b0-97ac-dd138100cf7c&blah')).toBeTruthy();
|
// range []
|
||||||
|
expect(globToRegex('**/api/v[0-9]').test('http://example.com/api/v1')).toBeTruthy();
|
||||||
|
expect(globToRegex('**/api/v[0-9]').test('http://example.com/api/version')).toBeFalsy();
|
||||||
|
|
||||||
|
// query params
|
||||||
|
expect(globToRegex('**/api\\?param').test('http://example.com/api?param')).toBeTruthy();
|
||||||
|
expect(globToRegex('**/api\\?param').test('http://example.com/api-param')).toBeFalsy();
|
||||||
|
expect(globToRegex('**/three-columns/settings.html\\?**id=[a-z]**').test('http://mydomain:8080/blah/blah/three-columns/settings.html?id=settings-e3c58efe-02e9-44b0-97ac-dd138100cf7c&blah')).toBeTruthy();
|
||||||
|
|
||||||
expect(globToRegex('\\?')).toEqual(/^\?$/);
|
expect(globToRegex('\\?')).toEqual(/^\?$/);
|
||||||
expect(globToRegex('\\')).toEqual(/^\\$/);
|
expect(globToRegex('\\')).toEqual(/^\\$/);
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,22 @@ it('should combine visible with other selectors', async ({ page }) => {
|
||||||
await expect(page.locator('.item >> visible=true >> text=data3')).toHaveText('visible data3');
|
await expect(page.locator('.item >> visible=true >> text=data3')).toHaveText('visible data3');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support filter(visible)', async ({ page }) => {
|
||||||
|
await page.setContent(`<div>
|
||||||
|
<div class="item" style="display: none">Hidden data0</div>
|
||||||
|
<div class="item">visible data1</div>
|
||||||
|
<div class="item" style="display: none">Hidden data1</div>
|
||||||
|
<div class="item">visible data2</div>
|
||||||
|
<div class="item" style="display: none">Hidden data2</div>
|
||||||
|
<div class="item">visible data3</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
const locator = page.locator('.item').filter({ visible: true }).nth(1);
|
||||||
|
await expect(locator).toHaveText('visible data2');
|
||||||
|
await expect(page.locator('.item').filter({ visible: true }).getByText('data3')).toHaveText('visible data3');
|
||||||
|
await expect(page.locator('.item').filter({ visible: false }).getByText('data1')).toHaveText('Hidden data1');
|
||||||
|
});
|
||||||
|
|
||||||
it('locator.count should work with deleted Map in main world', async ({ page }) => {
|
it('locator.count should work with deleted Map in main world', async ({ page }) => {
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/11254' });
|
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/11254' });
|
||||||
await page.evaluate('Map = 1');
|
await page.evaluate('Map = 1');
|
||||||
|
|
|
||||||
|
|
@ -515,6 +515,7 @@ it('should normalize whitespace', async ({ page }) => {
|
||||||
<summary> one \n two <a href="#"> link \n 1 </a> </summary>
|
<summary> one \n two <a href="#"> link \n 1 </a> </summary>
|
||||||
</details>
|
</details>
|
||||||
<input value=' hello world '>
|
<input value=' hello world '>
|
||||||
|
<button>hello\u00ad\u200bworld</button>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await checkAndMatchSnapshot(page.locator('body'), `
|
await checkAndMatchSnapshot(page.locator('body'), `
|
||||||
|
|
@ -522,6 +523,7 @@ it('should normalize whitespace', async ({ page }) => {
|
||||||
- text: one two
|
- text: one two
|
||||||
- link "link 1"
|
- link "link 1"
|
||||||
- textbox: hello world
|
- textbox: hello world
|
||||||
|
- button "helloworld"
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Weird whitespace in the template should be normalized.
|
// Weird whitespace in the template should be normalized.
|
||||||
|
|
@ -532,6 +534,7 @@ it('should normalize whitespace', async ({ page }) => {
|
||||||
two
|
two
|
||||||
- link " link 1 "
|
- link " link 1 "
|
||||||
- textbox: hello world
|
- textbox: hello world
|
||||||
|
- button "he\u00adlloworld\u200b"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ it('should check the box @smoke', async ({ page }) => {
|
||||||
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
||||||
await page.check('input');
|
await page.check('input');
|
||||||
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
|
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
|
||||||
|
await expect(page.locator('input[type="checkbox"]')).toBeChecked({ timeout: 1000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not check the checked box', async ({ page }) => {
|
it('should not check the checked box', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,14 @@ test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
|
test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.spec.ts-snapshots/test.yml': `
|
'a.spec.ts-snapshots/test.snapshot.yml': `
|
||||||
- heading "hello world"
|
- heading "hello world"
|
||||||
`,
|
`,
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
test('test', async ({ page }) => {
|
||||||
await page.setContent(\`<h1>hello world</h1>\`);
|
await page.setContent(\`<h1>hello world</h1>\`);
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.snapshot.yml' });
|
||||||
});
|
});
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
@ -43,66 +43,66 @@ test('should generate multiple missing', async ({ runInlineTest }, testInfo) =>
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
test('test', async ({ page }) => {
|
||||||
await page.setContent(\`<h1>hello world</h1>\`);
|
await page.setContent(\`<h1>hello world</h1>\`);
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-1.yml' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-1.snapshot.yml' });
|
||||||
await page.setContent(\`<h1>hello world 2</h1>\`);
|
await page.setContent(\`<h1>hello world 2</h1>\`);
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-2.yml' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-2.snapshot.yml' });
|
||||||
});
|
});
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-1.yml, writing actual`);
|
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-1.snapshot.yml, writing actual`);
|
||||||
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-2.yml, writing actual`);
|
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-2.snapshot.yml, writing actual`);
|
||||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
|
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.snapshot.yml'), 'utf8');
|
||||||
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
||||||
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.yml'), 'utf8');
|
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.snapshot.yml'), 'utf8');
|
||||||
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
|
test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.spec.ts-snapshots/test-1.yml': `
|
'a.spec.ts-snapshots/test-1.snapshot.yml': `
|
||||||
- heading "foo"
|
- heading "foo"
|
||||||
`,
|
`,
|
||||||
'a.spec.ts-snapshots/test-2.yml': `
|
'a.spec.ts-snapshots/test-2.snapshot.yml': `
|
||||||
- heading "bar"
|
- heading "bar"
|
||||||
`,
|
`,
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
test('test', async ({ page }) => {
|
||||||
await page.setContent(\`<h1>hello world</h1>\`);
|
await page.setContent(\`<h1>hello world</h1>\`);
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-1.yml' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-1.snapshot.yml' });
|
||||||
await page.setContent(\`<h1>hello world 2</h1>\`);
|
await page.setContent(\`<h1>hello world 2</h1>\`);
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-2.yml' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-2.snapshot.yml' });
|
||||||
});
|
});
|
||||||
`
|
`
|
||||||
}, { 'update-snapshots': 'all' });
|
}, { 'update-snapshots': 'all' });
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-1.yml`);
|
expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-1.snapshot.yml`);
|
||||||
expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-2.yml`);
|
expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-2.snapshot.yml`);
|
||||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
|
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.snapshot.yml'), 'utf8');
|
||||||
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
||||||
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.yml'), 'utf8');
|
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.snapshot.yml'), 'utf8');
|
||||||
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
|
test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.spec.ts-snapshots/test.yml': `
|
'a.spec.ts-snapshots/test.snapshot.yml': `
|
||||||
- heading "hello world"
|
- heading "hello world"
|
||||||
`,
|
`,
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
test('test', async ({ page }) => {
|
||||||
await page.setContent(\`<h1>hello world</h1>\`);
|
await page.setContent(\`<h1>hello world</h1>\`);
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.snapshot.yml' });
|
||||||
});
|
});
|
||||||
`
|
`
|
||||||
}, { 'update-snapshots': 'changed' });
|
}, { 'update-snapshots': 'changed' });
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test.yml'), 'utf8');
|
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test.snapshot.yml'), 'utf8');
|
||||||
expect(snapshot1.trim()).toBe('- heading "hello world"');
|
expect(snapshot1.trim()).toBe('- heading "hello world"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -120,14 +120,32 @@ test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-1.yml, writing actual`);
|
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-1.snapshot.yml, writing actual`);
|
||||||
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-2.yml, writing actual`);
|
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-2.snapshot.yml, writing actual`);
|
||||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-1.yml'), 'utf8');
|
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-1.snapshot.yml'), 'utf8');
|
||||||
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
||||||
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-2.yml'), 'utf8');
|
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-2.snapshot.yml'), 'utf8');
|
||||||
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('backwards compat with .yml extension', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.spec.ts-snapshots/test-1.yml': `
|
||||||
|
- heading "hello old world"
|
||||||
|
`,
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<h1>hello new world</h1>\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': 'changed' });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-1.yml.`);
|
||||||
|
});
|
||||||
|
|
||||||
for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
|
for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
|
||||||
test(`should update snapshot with the update-snapshots=${updateSnapshots} (config)`, async ({ runInlineTest }, testInfo) => {
|
test(`should update snapshot with the update-snapshots=${updateSnapshots} (config)`, async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
|
|
@ -143,13 +161,13 @@ for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
'a.spec.ts-snapshots/test-1.yml': '- heading "Old content" [level=1]',
|
'a.spec.ts-snapshots/test-1.snapshot.yml': '- heading "Old content" [level=1]',
|
||||||
});
|
});
|
||||||
|
|
||||||
const rebase = updateSnapshots === 'all' || updateSnapshots === 'changed';
|
const rebase = updateSnapshots === 'all' || updateSnapshots === 'changed';
|
||||||
expect(result.exitCode).toBe(rebase ? 0 : 1);
|
expect(result.exitCode).toBe(rebase ? 0 : 1);
|
||||||
if (rebase) {
|
if (rebase) {
|
||||||
const snapshotOutputPath = testInfo.outputPath('a.spec.ts-snapshots/test-1.yml');
|
const snapshotOutputPath = testInfo.outputPath('a.spec.ts-snapshots/test-1.snapshot.yml');
|
||||||
expect(result.output).toContain(`A snapshot is generated at`);
|
expect(result.output).toContain(`A snapshot is generated at`);
|
||||||
const data = fs.readFileSync(snapshotOutputPath);
|
const data = fs.readFileSync(snapshotOutputPath);
|
||||||
expect(data.toString()).toBe('- heading "New content" [level=1]');
|
expect(data.toString()).toBe('- heading "New content" [level=1]');
|
||||||
|
|
@ -169,7 +187,7 @@ test('should respect timeout', async ({ runInlineTest }, testInfo) => {
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
'a.spec.ts-snapshots/test-1.yml': '- heading "new world" [level=1]',
|
'a.spec.ts-snapshots/test-1.snapshot.yml': '- heading "new world" [level=1]',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
|
|
@ -183,14 +201,14 @@ test('should respect config.snapshotPathTemplate', async ({ runInlineTest }, tes
|
||||||
snapshotPathTemplate: 'my-snapshots/{testFilePath}/{arg}{ext}',
|
snapshotPathTemplate: 'my-snapshots/{testFilePath}/{arg}{ext}',
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'my-snapshots/dir/a.spec.ts/test.yml': `
|
'my-snapshots/dir/a.spec.ts/test.snapshot.yml': `
|
||||||
- heading "hello world"
|
- heading "hello world"
|
||||||
`,
|
`,
|
||||||
'dir/a.spec.ts': `
|
'dir/a.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
test('test', async ({ page }) => {
|
||||||
await page.setContent(\`<h1>hello world</h1>\`);
|
await page.setContent(\`<h1>hello world</h1>\`);
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.snapshot.yml' });
|
||||||
});
|
});
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
@ -210,17 +228,17 @@ test('should respect config.expect.toMatchAriaSnapshot.pathTemplate', async ({ r
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'my-snapshots/dir/a.spec.ts/test.yml': `
|
'my-snapshots/dir/a.spec.ts/test.snapshot.yml': `
|
||||||
- heading "wrong one"
|
- heading "wrong one"
|
||||||
`,
|
`,
|
||||||
'actual-snapshots/dir/a.spec.ts/test.yml': `
|
'actual-snapshots/dir/a.spec.ts/test.snapshot.yml': `
|
||||||
- heading "hello world"
|
- heading "hello world"
|
||||||
`,
|
`,
|
||||||
'dir/a.spec.ts': `
|
'dir/a.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
test('test', async ({ page }) => {
|
||||||
await page.setContent(\`<h1>hello world</h1>\`);
|
await page.setContent(\`<h1>hello world</h1>\`);
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.snapshot.yml' });
|
||||||
});
|
});
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -435,29 +435,29 @@ test('should work with pageSnapshot: on', async ({ runInlineTest }, testInfo) =>
|
||||||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||||
'.last-run.json',
|
'.last-run.json',
|
||||||
'artifacts-failing',
|
'artifacts-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
'artifacts-own-context-failing',
|
'artifacts-own-context-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
'artifacts-own-context-passing',
|
'artifacts-own-context-passing',
|
||||||
' test-finished-1.ariasnapshot',
|
' test-finished-1.snapshot.yml',
|
||||||
'artifacts-passing',
|
'artifacts-passing',
|
||||||
' test-finished-1.ariasnapshot',
|
' test-finished-1.snapshot.yml',
|
||||||
'artifacts-persistent-failing',
|
'artifacts-persistent-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
'artifacts-persistent-passing',
|
'artifacts-persistent-passing',
|
||||||
' test-finished-1.ariasnapshot',
|
' test-finished-1.snapshot.yml',
|
||||||
'artifacts-shared-shared-failing',
|
'artifacts-shared-shared-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
' test-failed-2.ariasnapshot',
|
' test-failed-2.snapshot.yml',
|
||||||
'artifacts-shared-shared-passing',
|
'artifacts-shared-shared-passing',
|
||||||
' test-finished-1.ariasnapshot',
|
' test-finished-1.snapshot.yml',
|
||||||
' test-finished-2.ariasnapshot',
|
' test-finished-2.snapshot.yml',
|
||||||
'artifacts-two-contexts',
|
'artifacts-two-contexts',
|
||||||
' test-finished-1.ariasnapshot',
|
' test-finished-1.snapshot.yml',
|
||||||
' test-finished-2.ariasnapshot',
|
' test-finished-2.snapshot.yml',
|
||||||
'artifacts-two-contexts-failing',
|
'artifacts-two-contexts-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
' test-failed-2.ariasnapshot',
|
' test-failed-2.snapshot.yml',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -475,16 +475,16 @@ test('should work with pageSnapshot: only-on-failure', async ({ runInlineTest },
|
||||||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||||
'.last-run.json',
|
'.last-run.json',
|
||||||
'artifacts-failing',
|
'artifacts-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
'artifacts-own-context-failing',
|
'artifacts-own-context-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
'artifacts-persistent-failing',
|
'artifacts-persistent-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
'artifacts-shared-shared-failing',
|
'artifacts-shared-shared-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
' test-failed-2.ariasnapshot',
|
' test-failed-2.snapshot.yml',
|
||||||
'artifacts-two-contexts-failing',
|
'artifacts-two-contexts-failing',
|
||||||
' test-failed-1.ariasnapshot',
|
' test-failed-1.snapshot.yml',
|
||||||
' test-failed-2.ariasnapshot',
|
' test-failed-2.snapshot.yml',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1187,12 +1187,11 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should include metadata with populateGitInfo = true', async ({ runInlineTest, writeFiles, showReport, page }) => {
|
test('should include metadata with gitCommit', async ({ runInlineTest, writeFiles, showReport, page }) => {
|
||||||
const files = {
|
const files = {
|
||||||
'uncommitted.txt': `uncommitted file`,
|
'uncommitted.txt': `uncommitted file`,
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default {
|
export default {
|
||||||
populateGitInfo: true,
|
|
||||||
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
|
|
@ -1220,6 +1219,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
|
|
||||||
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
||||||
PLAYWRIGHT_HTML_OPEN: 'never',
|
PLAYWRIGHT_HTML_OPEN: 'never',
|
||||||
|
GITHUB_ACTIONS: '1',
|
||||||
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
||||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
GITHUB_SERVER_URL: 'https://playwright.dev',
|
||||||
GITHUB_SHA: 'example-sha',
|
GITHUB_SHA: 'example-sha',
|
||||||
|
|
@ -1230,19 +1230,22 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
await page.getByRole('button', { name: 'Metadata' }).click();
|
await page.getByRole('button', { name: 'Metadata' }).click();
|
||||||
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
||||||
- 'link "chore(html): make this test look nice"'
|
- list:
|
||||||
- text: /^William <shakespeare@example.local> on/
|
- listitem:
|
||||||
- link /^[a-f0-9]{7}$/
|
- 'link "chore(html): make this test look nice"'
|
||||||
- text: 'foo : value1 bar : {"prop":"value2"} baz : ["value3",123]'
|
- listitem: /William <shakespeare@example\\.local>/
|
||||||
|
- list:
|
||||||
|
- listitem: "foo : value1"
|
||||||
|
- listitem: "bar : {\\"prop\\":\\"value2\\"}"
|
||||||
|
- listitem: "baz : [\\"value3\\",123]"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should include metadata with populateGitInfo on GHA', async ({ runInlineTest, writeFiles, showReport, page }) => {
|
test('should include metadata on GHA', async ({ runInlineTest, writeFiles, showReport, page }) => {
|
||||||
const files = {
|
const files = {
|
||||||
'uncommitted.txt': `uncommitted file`,
|
'uncommitted.txt': `uncommitted file`,
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default {
|
export default {
|
||||||
populateGitInfo: true,
|
|
||||||
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
|
|
@ -1279,6 +1282,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
|
|
||||||
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
||||||
PLAYWRIGHT_HTML_OPEN: 'never',
|
PLAYWRIGHT_HTML_OPEN: 'never',
|
||||||
|
GITHUB_ACTIONS: '1',
|
||||||
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
||||||
GITHUB_RUN_ID: 'example-run-id',
|
GITHUB_RUN_ID: 'example-run-id',
|
||||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
GITHUB_SERVER_URL: 'https://playwright.dev',
|
||||||
|
|
@ -1291,18 +1295,21 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
await page.getByRole('button', { name: 'Metadata' }).click();
|
await page.getByRole('button', { name: 'Metadata' }).click();
|
||||||
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
||||||
- 'link "My PR"'
|
- list:
|
||||||
- text: /^William <shakespeare@example.local> on/
|
- listitem:
|
||||||
- link "Logs"
|
- link "My PR"
|
||||||
- link "Pull Request"
|
- listitem: /William <shakespeare@example.local>/
|
||||||
- text: 'foo : value1 bar : {"prop":"value2"} baz : ["value3",123]'
|
- list:
|
||||||
|
- listitem: "foo : value1"
|
||||||
|
- listitem: "bar : {\\"prop\\":\\"value2\\"}"
|
||||||
|
- listitem: "baz : [\\"value3\\",123]"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not include git metadata with populateGitInfo = false', async ({ runInlineTest, showReport, page }) => {
|
test('should not include git metadata w/o gitCommit', async ({ runInlineTest, showReport, page }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default { populateGitInfo: false };
|
export default {};
|
||||||
`,
|
`,
|
||||||
'example.spec.ts': `
|
'example.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
@ -1323,7 +1330,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default {
|
export default {
|
||||||
metadata: {
|
metadata: {
|
||||||
'git.commit.info': { 'revision.timestamp': 'hi' }
|
gitCommit: { author: { date: 'hi' } }
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
|
|
@ -2757,8 +2764,11 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
'uncommitted.txt': `uncommitted file`,
|
'uncommitted.txt': `uncommitted file`,
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default {
|
export default {
|
||||||
populateGitInfo: true,
|
metadata: {
|
||||||
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
foo: 'value1',
|
||||||
|
bar: { prop: 'value2' },
|
||||||
|
baz: ['value3', 123]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'example.spec.ts': `
|
'example.spec.ts': `
|
||||||
|
|
@ -2788,6 +2798,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
|
|
||||||
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
||||||
PLAYWRIGHT_HTML_OPEN: 'never',
|
PLAYWRIGHT_HTML_OPEN: 'never',
|
||||||
|
GITHUB_ACTIONS: '1',
|
||||||
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
||||||
GITHUB_RUN_ID: 'example-run-id',
|
GITHUB_RUN_ID: 'example-run-id',
|
||||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
GITHUB_SERVER_URL: 'https://playwright.dev',
|
||||||
|
|
|
||||||
|
|
@ -1520,9 +1520,7 @@ pw:api | browserContext.newPage
|
||||||
test.step |custom step @ a.test.ts:4
|
test.step |custom step @ a.test.ts:4
|
||||||
pw:api | page.route @ a.test.ts:5
|
pw:api | page.route @ a.test.ts:5
|
||||||
pw:api | page.goto(${server.EMPTY_PAGE}) @ a.test.ts:12
|
pw:api | page.goto(${server.EMPTY_PAGE}) @ a.test.ts:12
|
||||||
pw:api | apiResponse.text @ a.test.ts:7
|
|
||||||
expect | expect.toBe @ a.test.ts:8
|
expect | expect.toBe @ a.test.ts:8
|
||||||
pw:api | apiResponse.text @ a.test.ts:9
|
|
||||||
hook |After Hooks
|
hook |After Hooks
|
||||||
fixture | fixture: page
|
fixture | fixture: page
|
||||||
fixture | fixture: context
|
fixture | fixture: context
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,26 @@ test('should ignore test.setTimeout when debugging', async ({ runInlineTest }) =
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should ignore globalTimeout when debugging', {
|
||||||
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34911' },
|
||||||
|
}, async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
export default {
|
||||||
|
globalTimeout: 100,
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('my test', async ({ }) => {
|
||||||
|
await new Promise(f => setTimeout(f, 2000));
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { debug: true });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should respect fixture timeout', async ({ runInlineTest }) => {
|
test('should respect fixture timeout', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,13 @@ test('should render html report git info metadata', async ({ runUITest }) => {
|
||||||
'reporter.ts': `
|
'reporter.ts': `
|
||||||
module.exports = class Reporter {
|
module.exports = class Reporter {
|
||||||
onBegin(config, suite) {
|
onBegin(config, suite) {
|
||||||
console.log('ci.link:', config.metadata['git.commit.info']['ci.link']);
|
console.log('ci.link:', config.metadata['ci'].commitHref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
import { defineConfig } from '@playwright/test';
|
import { defineConfig } from '@playwright/test';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
populateGitInfo: true,
|
|
||||||
reporter: './reporter.ts',
|
reporter: './reporter.ts',
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
|
|
@ -37,6 +36,7 @@ test('should render html report git info metadata', async ({ runUITest }) => {
|
||||||
test('should work', async ({}) => {});
|
test('should work', async ({}) => {});
|
||||||
`
|
`
|
||||||
}, {
|
}, {
|
||||||
|
JENKINS_URL: '1',
|
||||||
BUILD_URL: 'https://playwright.dev',
|
BUILD_URL: 'https://playwright.dev',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -503,11 +503,11 @@ test('skipped steps should have an indicator', async ({ runUITest }) => {
|
||||||
test('should show copy prompt button in errors tab', async ({ runUITest }) => {
|
test('should show copy prompt button in errors tab', async ({ runUITest }) => {
|
||||||
const { page } = await runUITest({
|
const { page } = await runUITest({
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('fails', async () => {
|
test('fails', async () => {
|
||||||
expect(1).toBe(2);
|
expect(1).toBe(2);
|
||||||
});
|
});
|
||||||
`,
|
`.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByText('fails').dblclick();
|
await page.getByText('fails').dblclick();
|
||||||
|
|
@ -517,4 +517,11 @@ test('should show copy prompt button in errors tab', async ({ runUITest }) => {
|
||||||
await page.locator('.tab-errors').getByRole('button', { name: 'Copy as Prompt' }).click();
|
await page.locator('.tab-errors').getByRole('button', { name: 'Copy as Prompt' }).click();
|
||||||
const prompt = await page.evaluate(() => navigator.clipboard.readText());
|
const prompt = await page.evaluate(() => navigator.clipboard.readText());
|
||||||
expect(prompt, 'contains error').toContain('expect(received).toBe(expected)');
|
expect(prompt, 'contains error').toContain('expect(received).toBe(expected)');
|
||||||
|
expect(prompt, 'contains codeframe').toContain(`
|
||||||
|
1 | import { test, expect } from '@playwright/test';
|
||||||
|
2 | test('fails', async () => {
|
||||||
|
> 3 | expect(1).toBe(2);
|
||||||
|
^
|
||||||
|
4 | });
|
||||||
|
`.trim());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ class Workspace {
|
||||||
}
|
}
|
||||||
await maybeWriteJSON(pkg.packageJSONPath, pkg.packageJSON);
|
await maybeWriteJSON(pkg.packageJSONPath, pkg.packageJSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-run npm i to make package-lock dirty.
|
// Re-run npm i to make package-lock dirty.
|
||||||
child_process.execSync('npm i');
|
child_process.execSync('npm i');
|
||||||
return hasChanges;
|
return hasChanges;
|
||||||
|
|
@ -167,6 +167,11 @@ const workspace = new Workspace(ROOT_PATH, [
|
||||||
path: path.join(ROOT_PATH, 'packages', 'playwright-chromium'),
|
path: path.join(ROOT_PATH, 'packages', 'playwright-chromium'),
|
||||||
files: LICENCE_FILES,
|
files: LICENCE_FILES,
|
||||||
}),
|
}),
|
||||||
|
new PWPackage({
|
||||||
|
name: '@playwright/client',
|
||||||
|
path: path.join(ROOT_PATH, 'packages', 'playwright-client'),
|
||||||
|
files: LICENCE_FILES,
|
||||||
|
}),
|
||||||
new PWPackage({
|
new PWPackage({
|
||||||
name: '@playwright/experimental-tools',
|
name: '@playwright/experimental-tools',
|
||||||
path: path.join(ROOT_PATH, 'packages', 'playwright-tools'),
|
path: path.join(ROOT_PATH, 'packages', 'playwright-tools'),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue