Compare commits
22 commits
roll-into-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31f4a05eb6 | ||
|
|
7a61aa25e6 | ||
|
|
67d6f7f603 | ||
|
|
b0ceed51a5 | ||
|
|
08ea36caa2 | ||
|
|
70cc2b14e2 | ||
|
|
ad64f8d859 | ||
|
|
a1146fd4a3 | ||
|
|
3ce9ae6a7d | ||
|
|
837abfbc15 | ||
|
|
10fc0ef221 | ||
|
|
58db3f7e3f | ||
|
|
a803e6053a | ||
|
|
b5fe029c1b | ||
|
|
cd23a224f6 | ||
|
|
17c4d8e5ec | ||
|
|
a04a93c1fd | ||
|
|
439427c14e | ||
|
|
aaac9923fd | ||
|
|
411f938296 | ||
|
|
b148cbad76 | ||
|
|
a9bbf4b56d |
|
|
@ -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]>
|
||||||
|
|
@ -2478,18 +2481,6 @@ When all steps combined have not finished during the specified [`option: timeout
|
||||||
### option: Locator.uncheck.trial = %%-input-trial-%%
|
### option: Locator.uncheck.trial = %%-input-trial-%%
|
||||||
* since: v1.14
|
* since: v1.14
|
||||||
|
|
||||||
## method: Locator.visible
|
|
||||||
* since: v1.51
|
|
||||||
- returns: <[Locator]>
|
|
||||||
|
|
||||||
Returns a locator that only matches [visible](../actionability.md#visible) elements.
|
|
||||||
|
|
||||||
### option: Locator.visible.visible
|
|
||||||
* since: v1.51
|
|
||||||
- `visible` <[boolean]>
|
|
||||||
|
|
||||||
Whether to match visible or invisible elements.
|
|
||||||
|
|
||||||
## async method: Locator.waitFor
|
## async method: Locator.waitFor
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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').visible().click();
|
await page.locator('button').filter({ visible: true }).click();
|
||||||
```
|
```
|
||||||
```java
|
```java
|
||||||
page.locator("button").visible().click();
|
page.locator("button").filter(new Locator.FilterOptions.setVisible(true)).click();
|
||||||
```
|
```
|
||||||
```python async
|
```python async
|
||||||
await page.locator("button").visible().click()
|
await page.locator("button").filter(visible=True).click()
|
||||||
```
|
```
|
||||||
```python sync
|
```python sync
|
||||||
page.locator("button").visible().click()
|
page.locator("button").filter(visible=True).click()
|
||||||
```
|
```
|
||||||
```csharp
|
```csharp
|
||||||
await page.Locator("button").Visible().ClickAsync();
|
await page.Locator("button").Filter(new() { Visible = true }).ClickAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lists
|
## Lists
|
||||||
|
|
|
||||||
|
|
@ -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">>
|
||||||
|
|
@ -446,7 +429,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.
|
||||||
|
|
||||||
|
|
@ -680,7 +663,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,
|
||||||
},
|
},
|
||||||
|
|
@ -709,19 +692,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',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,11 @@ export default defineConfig({
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
snapshotPathTemplate: '{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}',
|
snapshotPathTemplate: '{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}',
|
||||||
reporter: process.env.CI ? 'blob' : 'html',
|
reporter: process.env.CI ? [
|
||||||
|
['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }],
|
||||||
|
] : [
|
||||||
|
['html']
|
||||||
|
],
|
||||||
use: {
|
use: {
|
||||||
ctPort: 3101,
|
ctPort: 3101,
|
||||||
ctViteConfig: {
|
ctViteConfig: {
|
||||||
|
|
|
||||||
|
|
@ -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>}
|
||||||
|
|
|
||||||
301
packages/playwright-client/types/types.d.ts
vendored
301
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -14615,17 +14525,6 @@ export interface Locator {
|
||||||
trial?: boolean;
|
trial?: boolean;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a locator that only matches [visible](https://playwright.dev/docs/actionability#visible) elements.
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
visible(options?: {
|
|
||||||
/**
|
|
||||||
* Whether to match visible or invisible elements.
|
|
||||||
*/
|
|
||||||
visible?: boolean;
|
|
||||||
}): Locator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns when element specified by locator satisfies the
|
* Returns when element specified by locator satisfies the
|
||||||
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.
|
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.
|
||||||
|
|
@ -14824,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.
|
||||||
*
|
*
|
||||||
|
|
@ -16720,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.
|
||||||
*
|
*
|
||||||
|
|
@ -17572,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)
|
||||||
|
|
@ -17653,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.
|
||||||
|
|
@ -17754,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;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18576,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;
|
||||||
|
|
||||||
|
|
@ -18618,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;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
@ -22157,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),
|
||||||
|
|
@ -22518,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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -218,11 +222,6 @@ export class Locator implements api.Locator {
|
||||||
return new Locator(this._frame, this._selector + ` >> nth=${index}`);
|
return new Locator(this._frame, this._selector + ` >> nth=${index}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
visible(options: { visible?: boolean } = {}): Locator {
|
|
||||||
const { visible = true } = options;
|
|
||||||
return new Locator(this._frame, this._selector + ` >> visible=${visible ? 'true' : 'false'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
and(locator: Locator): Locator {
|
and(locator: Locator): Locator {
|
||||||
if (locator._frame !== this._frame)
|
if (locator._frame !== this._frame)
|
||||||
throw new Error(`Locators must belong to the same frame.`);
|
throw new Error(`Locators must belong to the same 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,
|
||||||
|
|
|
||||||
|
|
@ -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: []
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 : [] })),
|
||||||
|
|
|
||||||
|
|
@ -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>';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,7 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||||
case 'last':
|
case 'last':
|
||||||
return `last()`;
|
return `last()`;
|
||||||
case 'visible':
|
case 'visible':
|
||||||
return `visible(${body === 'true' ? '' : '{ visible: false }'})`;
|
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)) {
|
||||||
|
|
@ -376,7 +376,7 @@ export class PythonLocatorFactory implements LocatorFactory {
|
||||||
case 'last':
|
case 'last':
|
||||||
return `last`;
|
return `last`;
|
||||||
case 'visible':
|
case 'visible':
|
||||||
return `visible(${body === 'true' ? '' : 'visible=False'})`;
|
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)) {
|
||||||
|
|
@ -485,7 +485,7 @@ export class JavaLocatorFactory implements LocatorFactory {
|
||||||
case 'last':
|
case 'last':
|
||||||
return `last()`;
|
return `last()`;
|
||||||
case 'visible':
|
case 'visible':
|
||||||
return `visible(${body === 'true' ? '' : `new ${clazz}.VisibleOptions().setVisible(false)`})`;
|
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)) {
|
||||||
|
|
@ -584,7 +584,7 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||||
case 'last':
|
case 'last':
|
||||||
return `Last`;
|
return `Last`;
|
||||||
case 'visible':
|
case 'visible':
|
||||||
return `Visible(${body === 'true' ? '' : 'new() { Visible = false }'})`;
|
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,9 +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(/visible\(,?visible=true\)/g, 'visible=true')
|
.replace(/filter\(,?visible=true\)/g, 'visible=true')
|
||||||
.replace(/visible\(,?visible=false\)/g, 'visible=false')
|
.replace(/filter\(,?visible=false\)/g, 'visible=false')
|
||||||
.replace(/visible\(\)/g, 'visible=true')
|
|
||||||
.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;
|
||||||
|
|
|
||||||
301
packages/playwright-core/types/types.d.ts
vendored
301
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -14615,17 +14525,6 @@ export interface Locator {
|
||||||
trial?: boolean;
|
trial?: boolean;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a locator that only matches [visible](https://playwright.dev/docs/actionability#visible) elements.
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
visible(options?: {
|
|
||||||
/**
|
|
||||||
* Whether to match visible or invisible elements.
|
|
||||||
*/
|
|
||||||
visible?: boolean;
|
|
||||||
}): Locator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns when element specified by locator satisfies the
|
* Returns when element specified by locator satisfies the
|
||||||
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.
|
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.
|
||||||
|
|
@ -14824,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.
|
||||||
*
|
*
|
||||||
|
|
@ -16720,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.
|
||||||
*
|
*
|
||||||
|
|
@ -17572,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)
|
||||||
|
|
@ -17653,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.
|
||||||
|
|
@ -17754,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;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18576,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;
|
||||||
|
|
||||||
|
|
@ -18618,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;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
@ -22157,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),
|
||||||
|
|
@ -22518,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;
|
|
||||||
cliArgs: string[] = [];
|
cliArgs: string[] = [];
|
||||||
cliGrep: string | undefined;
|
cliGrep: string | undefined;
|
||||||
cliGrepInvert: string | undefined;
|
cliGrepInvert: string | undefined;
|
||||||
|
|
@ -78,7 +77,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);
|
||||||
|
|
@ -94,14 +92,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),
|
||||||
|
|
@ -301,7 +299,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,170 @@
|
||||||
* 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 commitInfoResult = await spawnAsync(
|
const tokens = [
|
||||||
'git',
|
'%H', // commit hash
|
||||||
['show', '-s', `--format=%H${separator}%s${separator}%an${separator}%ae${separator}%ct`, 'HEAD'],
|
'%h', // abbreviated commit hash
|
||||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
'%s', // subject
|
||||||
);
|
'%B', // raw body (unwrapped subject and body)
|
||||||
if (commitInfoResult.code)
|
'%an', // author name
|
||||||
return;
|
'%ae', // author email
|
||||||
const showOutput = commitInfoResult.stdout.trim();
|
'%at', // author date, UNIX timestamp
|
||||||
const [id, subject, author, email, rawTimestamp] = showOutput.split(separator);
|
'%cn', // committer name
|
||||||
let timestamp: number = Number.parseInt(rawTimestamp, 10);
|
'%ce', // committer email
|
||||||
timestamp = Number.isInteger(timestamp) ? timestamp * 1000 : 0;
|
'%ct', // committer date, UNIX timestamp
|
||||||
|
'', // branch
|
||||||
|
];
|
||||||
|
const output = await runGit(`git log -1 --pretty=format:"${tokens.join(separator)}" && git rev-parse --abbrev-ref HEAD`, gitDir);
|
||||||
|
if (!output)
|
||||||
|
return undefined;
|
||||||
|
const [hash, shortHash, subject, body, authorName, authorEmail, authorTime, committerName, committerEmail, committerTime, branch] = output.split(separator);
|
||||||
|
|
||||||
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
|
async function gitDiff(gitDir: string, ci?: CIInfo): Promise<string | undefined> {
|
||||||
if (envInfo['pull.base']) {
|
const diffLimit = 100_000;
|
||||||
const pullDiffResult = await spawnAsync(
|
if (ci) {
|
||||||
'git',
|
// First try the diff against the base branch.
|
||||||
['diff', envInfo['pull.base']],
|
const diff = await runGit(`git diff ${ci.baseHash}`, gitDir);
|
||||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
if (diff)
|
||||||
);
|
return diff.substring(0, diffLimit);
|
||||||
if (!pullDiffResult.code)
|
|
||||||
result['pull.diff'] = pullDiffResult.stdout.substring(0, diffLimit);
|
// Grow history for shallow checkout.
|
||||||
} else {
|
const output = await runGit('git fetch --deepen=1 && git show HEAD', gitDir);
|
||||||
const diffResult = await spawnAsync(
|
return output?.substring(0, diffLimit);
|
||||||
'git',
|
|
||||||
['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;
|
// Check dirty state first.
|
||||||
|
const uncommitted = await runGit('git diff', gitDir);
|
||||||
|
if (uncommitted)
|
||||||
|
return uncommitted.substring(0, diffLimit);
|
||||||
|
|
||||||
|
// Assume non-shallow checkout on local.
|
||||||
|
const diff = await runGit('git diff HEAD~1', gitDir);
|
||||||
|
return diff?.substring(0, diffLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runGit(command: string, cwd: string): Promise<string | undefined> {
|
||||||
|
const result = await spawnAsync(
|
||||||
|
command,
|
||||||
|
[],
|
||||||
|
{ stdio: 'pipe', cwd, timeout: GIT_OPERATIONS_TIMEOUT_MS, shell: true }
|
||||||
|
);
|
||||||
|
if (result.code) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`Failed to run ${command}: ${result.stderr}`);
|
||||||
|
}
|
||||||
|
return result.code ? undefined : result.stdout.trim();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,3 @@ export type TestRunnerPluginRegistration = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export { webServer } from './webServerPlugin';
|
export { webServer } from './webServerPlugin';
|
||||||
export { gitCommitInfo } from './gitCommitInfoPlugin';
|
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,8 @@ export class TerminalReporter implements ReporterV2 {
|
||||||
if (full && summary.failuresToPrint.length && !this._omitFailures)
|
if (full && summary.failuresToPrint.length && !this._omitFailures)
|
||||||
this._printFailures(summary.failuresToPrint);
|
this._printFailures(summary.failuresToPrint);
|
||||||
this._printSlowTests();
|
this._printSlowTests();
|
||||||
this._printWarnings();
|
// TODO: 1.52: Make warning display prettier
|
||||||
|
// this._printWarnings();
|
||||||
this._printSummary(summaryMessage);
|
this._printSummary(summaryMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ export class FloatingPromiseScope {
|
||||||
* **NOTE:** Returning from an async function wraps the result in a promise, regardless of whether the return value is a promise. This will automatically mark the promise as awaited. Avoid this.
|
* **NOTE:** Returning from an async function wraps the result in a promise, regardless of whether the return value is a promise. This will automatically mark the promise as awaited. Avoid this.
|
||||||
*/
|
*/
|
||||||
wrapPromiseAPIResult<T>(promise: Promise<T>): Promise<T> {
|
wrapPromiseAPIResult<T>(promise: Promise<T>): Promise<T> {
|
||||||
|
if (process.env.PW_DISABLE_FLOATING_PROMISES_WARNING)
|
||||||
|
return promise;
|
||||||
|
|
||||||
const promiseProxy = new Proxy(promise, {
|
const promiseProxy = new Proxy(promise, {
|
||||||
get: (target, prop, receiver) => {
|
get: (target, prop, receiver) => {
|
||||||
if (prop === 'then') {
|
if (prop === 'then') {
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
@ -324,14 +324,17 @@ export class WorkerMain extends ProcessRunner {
|
||||||
|
|
||||||
// Create warning if any of the async calls were not awaited in various stages.
|
// Create warning if any of the async calls were not awaited in various stages.
|
||||||
const checkForFloatingPromises = (functionDescription: string) => {
|
const checkForFloatingPromises = (functionDescription: string) => {
|
||||||
|
if (process.env.PW_DISABLE_FLOATING_PROMISES_WARNING)
|
||||||
|
return;
|
||||||
if (!testInfo._floatingPromiseScope.hasFloatingPromises())
|
if (!testInfo._floatingPromiseScope.hasFloatingPromises())
|
||||||
return;
|
return;
|
||||||
testInfo.annotations.push({ type: 'warning', description: `Some async calls were not awaited by the end of ${functionDescription}. This can cause flakiness.` });
|
// TODO: 1.52: Actually build annotations
|
||||||
|
// testInfo.annotations.push({ type: 'warning', description: `Some async calls were not awaited by the end of ${functionDescription}. This can cause flakiness.` });
|
||||||
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 +359,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 +379,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;
|
||||||
|
|
@ -393,12 +396,12 @@ export class WorkerMain extends ProcessRunner {
|
||||||
// After hooks get an additional timeout.
|
// After hooks get an additional timeout.
|
||||||
const afterHooksTimeout = calculateMaxTimeout(this._project.project.timeout, testInfo.timeout);
|
const afterHooksTimeout = calculateMaxTimeout(this._project.project.timeout, testInfo.timeout);
|
||||||
const afterHooksSlot = { timeout: afterHooksTimeout, elapsed: 0 };
|
const afterHooksSlot = { timeout: afterHooksTimeout, elapsed: 0 };
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
@ -448,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.
|
||||||
|
|
@ -481,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.
|
||||||
|
|
||||||
|
|
@ -534,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 };
|
||||||
|
|
@ -587,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
|
||||||
|
|
@ -1465,7 +1443,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;
|
||||||
};
|
};
|
||||||
|
|
@ -1917,12 +1895,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;
|
||||||
};
|
};
|
||||||
|
|
@ -8815,14 +8793,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
|
||||||
|
|
@ -8924,13 +8902,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 (
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ import type { ReporterDescription } from '@playwright/test';
|
||||||
|
|
||||||
const reporters = () => {
|
const reporters = () => {
|
||||||
const result: ReporterDescription[] = process.env.CI ? [
|
const result: ReporterDescription[] = process.env.CI ? [
|
||||||
['html'],
|
['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }],
|
||||||
['blob'],
|
|
||||||
] : [
|
] : [
|
||||||
['html']
|
['html']
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -321,23 +321,17 @@ it('reverse engineer hasNotText', async ({ page }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reverse engineer visible', async ({ page }) => {
|
it('reverse engineer visible', async ({ page }) => {
|
||||||
expect.soft(generate(page.getByText('Hello').visible().locator('div'))).toEqual({
|
expect.soft(generate(page.getByText('Hello').filter({ visible: true }).locator('div'))).toEqual({
|
||||||
csharp: `GetByText("Hello").Visible().Locator("div")`,
|
csharp: `GetByText("Hello").Filter(new() { Visible = true }).Locator("div")`,
|
||||||
java: `getByText("Hello").visible().locator("div")`,
|
java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(true)).locator("div")`,
|
||||||
javascript: `getByText('Hello').visible().locator('div')`,
|
javascript: `getByText('Hello').filter({ visible: true }).locator('div')`,
|
||||||
python: `get_by_text("Hello").visible().locator("div")`,
|
python: `get_by_text("Hello").filter(visible=True).locator("div")`,
|
||||||
});
|
});
|
||||||
expect.soft(generate(page.getByText('Hello').visible({ visible: true }).locator('div'))).toEqual({
|
expect.soft(generate(page.getByText('Hello').filter({ visible: false }).locator('div'))).toEqual({
|
||||||
csharp: `GetByText("Hello").Visible().Locator("div")`,
|
csharp: `GetByText("Hello").Filter(new() { Visible = false }).Locator("div")`,
|
||||||
java: `getByText("Hello").visible().locator("div")`,
|
java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(false)).locator("div")`,
|
||||||
javascript: `getByText('Hello').visible().locator('div')`,
|
javascript: `getByText('Hello').filter({ visible: false }).locator('div')`,
|
||||||
python: `get_by_text("Hello").visible().locator("div")`,
|
python: `get_by_text("Hello").filter(visible=False).locator("div")`,
|
||||||
});
|
|
||||||
expect.soft(generate(page.getByText('Hello').visible({ visible: false }).locator('div'))).toEqual({
|
|
||||||
csharp: `GetByText("Hello").Visible(new() { Visible = false }).Locator("div")`,
|
|
||||||
java: `getByText("Hello").visible(new Locator.VisibleOptions().setVisible(false)).locator("div")`,
|
|
||||||
javascript: `getByText('Hello').visible({ visible: false }).locator('div')`,
|
|
||||||
python: `get_by_text("Hello").visible(visible=False).locator("div")`,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ 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 .visible()', async ({ page }) => {
|
it('should support filter(visible)', async ({ page }) => {
|
||||||
await page.setContent(`<div>
|
await page.setContent(`<div>
|
||||||
<div class="item" style="display: none">Hidden data0</div>
|
<div class="item" style="display: none">Hidden data0</div>
|
||||||
<div class="item">visible data1</div>
|
<div class="item">visible data1</div>
|
||||||
|
|
@ -160,11 +160,10 @@ it('should support .visible()', async ({ page }) => {
|
||||||
<div class="item">visible data3</div>
|
<div class="item">visible data3</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
const locator = page.locator('.item').visible().nth(1);
|
const locator = page.locator('.item').filter({ visible: true }).nth(1);
|
||||||
await expect(locator).toHaveText('visible data2');
|
await expect(locator).toHaveText('visible data2');
|
||||||
await expect(page.locator('.item').visible().getByText('data3')).toHaveText('visible data3');
|
await expect(page.locator('.item').filter({ visible: true }).getByText('data3')).toHaveText('visible data3');
|
||||||
await expect(page.locator('.item').visible({ visible: true }).getByText('data2')).toHaveText('visible data2');
|
await expect(page.locator('.item').filter({ visible: false }).getByText('data1')).toHaveText('Hidden data1');
|
||||||
await expect(page.locator('.item').visible({ 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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,249 +14,274 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from './playwright-test-fixtures';
|
// import { JSONReport } from 'packages/playwright-test/reporter';
|
||||||
|
// import { test, expect } from './playwright-test-fixtures';
|
||||||
|
|
||||||
const warningSnippet = 'Some async calls were not awaited';
|
// const warningSnippet = 'Some async calls were not awaited';
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
// test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
test.describe('await', () => {
|
// const getWarnings = (report: JSONReport) => report.suites.flatMap(s => s.specs).flatMap(s => s.tests).flatMap(t => t.annotations).filter(a => a.type === 'warning');
|
||||||
test('should not care about non-API promises', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test } from '@playwright/test';
|
|
||||||
test('test', () => {
|
|
||||||
new Promise(() => {});
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(0);
|
|
||||||
expect(stdout).not.toContain(warningSnippet);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should warn about missing await on expects when failing', async ({ runInlineTest }) => {
|
// test.describe('await', () => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// test('should not care about non-API promises', async ({ runInlineTest }) => {
|
||||||
'a.test.ts': `
|
// const { exitCode, report } = await runInlineTest({
|
||||||
import { test, expect } from '@playwright/test';
|
// 'a.test.ts': `
|
||||||
test('custom test name', async ({ page }) => {
|
// import { test } from '@playwright/test';
|
||||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
// test('test', () => {
|
||||||
});
|
// new Promise(() => {});
|
||||||
`
|
// });
|
||||||
});
|
// `
|
||||||
expect(exitCode).toBe(1);
|
// });
|
||||||
expect(stdout).toContain(warningSnippet);
|
// expect(exitCode).toBe(0);
|
||||||
expect(stdout).toContain('the test');
|
// const warnings = getWarnings(report);
|
||||||
expect(stdout).toContain('custom test name');
|
// expect(warnings.length).toEqual(0);
|
||||||
});
|
// });
|
||||||
|
|
||||||
test('should warn about missing await on expects when passing', async ({ runInlineTest }) => {
|
// test('should warn about missing await on expects when failing', async ({ runInlineTest }) => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('custom test name', async ({ page }) => {
|
||||||
await page.setContent('<div>A</div>');
|
// expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
||||||
expect(page.locator('div')).toHaveText('A');
|
// });
|
||||||
});
|
// `
|
||||||
`
|
// });
|
||||||
});
|
// expect(exitCode).toBe(1);
|
||||||
expect(exitCode).toBe(0);
|
// const warnings = getWarnings(report);
|
||||||
expect(stdout).toContain(warningSnippet);
|
// expect(warnings.length).toEqual(1);
|
||||||
});
|
// expect(warnings[0].description).toContain(warningSnippet);
|
||||||
|
// expect(warnings[0].description).toContain('the test');
|
||||||
|
// });
|
||||||
|
|
||||||
test('should not warn when not missing await on expects when failing', async ({ runInlineTest }) => {
|
// test('should warn about missing await on expects when passing', async ({ runInlineTest }) => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('test', async ({ page }) => {
|
||||||
await expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
// await page.setContent('<div>A</div>');
|
||||||
});
|
// expect(page.locator('div')).toHaveText('A');
|
||||||
`
|
// });
|
||||||
});
|
// `
|
||||||
expect(exitCode).toBe(1);
|
// });
|
||||||
expect(stdout).not.toContain(warningSnippet);
|
// expect(exitCode).toBe(0);
|
||||||
});
|
// const warnings = getWarnings(report);
|
||||||
|
// expect(warnings.length).toEqual(1);
|
||||||
|
// expect(warnings[0].description).toContain(warningSnippet);
|
||||||
|
// });
|
||||||
|
|
||||||
test('should not warn when not missing await on expects when passing', async ({ runInlineTest }) => {
|
// test('should not warn when not missing await on expects when failing', async ({ runInlineTest }) => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('test', async ({ page }) => {
|
||||||
await page.setContent('<div>A</div>');
|
// await expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
||||||
await expect(page.locator('div')).toHaveText('A');
|
// });
|
||||||
});
|
// `
|
||||||
`
|
// });
|
||||||
});
|
// expect(exitCode).toBe(1);
|
||||||
expect(exitCode).toBe(0);
|
// const warnings = getWarnings(report);
|
||||||
expect(stdout).not.toContain(warningSnippet);
|
// expect(warnings.length).toEqual(0);
|
||||||
});
|
// });
|
||||||
|
|
||||||
test('should not warn when using then on expects when passing', async ({ runInlineTest }) => {
|
// test('should not warn when not missing await on expects when passing', async ({ runInlineTest }) => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('test', async ({ page }) => {
|
||||||
await page.setContent('<div>A</div>');
|
// await page.setContent('<div>A</div>');
|
||||||
expect(page.locator('div')).toHaveText('A').then(() => {});
|
// await expect(page.locator('div')).toHaveText('A');
|
||||||
});
|
// });
|
||||||
`
|
// `
|
||||||
});
|
// });
|
||||||
expect(exitCode).toBe(0);
|
// expect(exitCode).toBe(0);
|
||||||
expect(stdout).not.toContain(warningSnippet);
|
// const warnings = getWarnings(report);
|
||||||
});
|
// expect(warnings.length).toEqual(0);
|
||||||
|
// });
|
||||||
|
|
||||||
test('should warn about missing await on reject', async ({ runInlineTest }) => {
|
// test('should not warn when using then on expects when passing', async ({ runInlineTest }) => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('test', async ({ page }) => {
|
||||||
expect(Promise.reject(new Error('foo'))).rejects.toThrow('foo');
|
// await page.setContent('<div>A</div>');
|
||||||
});
|
// expect(page.locator('div')).toHaveText('A').then(() => {});
|
||||||
`
|
// });
|
||||||
});
|
// `
|
||||||
expect(exitCode).toBe(0);
|
// });
|
||||||
expect(stdout).toContain(warningSnippet);
|
// expect(exitCode).toBe(0);
|
||||||
});
|
// const warnings = getWarnings(report);
|
||||||
|
// expect(warnings.length).toEqual(0);
|
||||||
|
// });
|
||||||
|
|
||||||
test('should warn about missing await on reject.not', async ({ runInlineTest }) => {
|
// test('should warn about missing await on reject', async ({ runInlineTest }) => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('test', async ({ page }) => {
|
||||||
expect(Promise.reject(new Error('foo'))).rejects.not.toThrow('foo');
|
// expect(Promise.reject(new Error('foo'))).rejects.toThrow('foo');
|
||||||
});
|
// });
|
||||||
`
|
// `
|
||||||
});
|
// });
|
||||||
expect(exitCode).toBe(1);
|
// expect(exitCode).toBe(0);
|
||||||
expect(stdout).toContain(warningSnippet);
|
// const warnings = getWarnings(report);
|
||||||
});
|
// expect(warnings.length).toEqual(1);
|
||||||
|
// expect(warnings[0].description).toContain(warningSnippet);
|
||||||
|
// });
|
||||||
|
|
||||||
test('should warn about missing await on test.step', async ({ runInlineTest }) => {
|
// test('should warn about missing await on reject.not', async ({ runInlineTest }) => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('test', async ({ page }) => {
|
||||||
await page.setContent('<div>A</div>');
|
// expect(Promise.reject(new Error('foo'))).rejects.not.toThrow('foo');
|
||||||
test.step('step', () => {});
|
// });
|
||||||
await expect(page.locator('div')).toHaveText('A');
|
// `
|
||||||
});
|
// });
|
||||||
`
|
// expect(exitCode).toBe(1);
|
||||||
});
|
// const warnings = getWarnings(report);
|
||||||
expect(exitCode).toBe(0);
|
// expect(warnings.length).toEqual(1);
|
||||||
expect(stdout).toContain(warningSnippet);
|
// expect(warnings[0].description).toContain(warningSnippet);
|
||||||
});
|
// });
|
||||||
|
|
||||||
test('should not warn when not missing await on test.step', async ({ runInlineTest }) => {
|
// test('should warn about missing await on test.step', async ({ runInlineTest }) => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('test', async ({ page }) => {
|
||||||
await page.setContent('<div>A</div>');
|
// await page.setContent('<div>A</div>');
|
||||||
await test.step('step', () => {});
|
// test.step('step', () => {});
|
||||||
await expect(page.locator('div')).toHaveText('A');
|
// await expect(page.locator('div')).toHaveText('A');
|
||||||
});
|
// });
|
||||||
`
|
// `
|
||||||
});
|
// });
|
||||||
expect(exitCode).toBe(0);
|
// expect(exitCode).toBe(0);
|
||||||
expect(stdout).not.toContain(warningSnippet);
|
// const warnings = getWarnings(report);
|
||||||
});
|
// expect(warnings.length).toEqual(1);
|
||||||
|
// expect(warnings[0].description).toContain(warningSnippet);
|
||||||
|
// });
|
||||||
|
|
||||||
test('should warn about missing await on test.step.skip', async ({ runInlineTest }) => {
|
// test('should not warn when not missing await on test.step', async ({ runInlineTest }) => {
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('test', async ({ page }) => {
|
||||||
await page.setContent('<div>A</div>');
|
// await page.setContent('<div>A</div>');
|
||||||
test.step.skip('step', () => {});
|
// await test.step('step', () => {});
|
||||||
await expect(page.locator('div')).toHaveText('A');
|
// await expect(page.locator('div')).toHaveText('A');
|
||||||
});
|
// });
|
||||||
`
|
// `
|
||||||
});
|
// });
|
||||||
expect(exitCode).toBe(0);
|
// expect(exitCode).toBe(0);
|
||||||
expect(stdout).toContain(warningSnippet);
|
// const warnings = getWarnings(report);
|
||||||
});
|
// expect(warnings.length).toEqual(0);
|
||||||
|
// });
|
||||||
|
|
||||||
test('traced promise should be instanceof Promise', async ({ runInlineTest }) => {
|
// test('should warn about missing await on test.step.skip', async ({ runInlineTest }) => {
|
||||||
const { exitCode } = await runInlineTest({
|
// const { exitCode, report } = await runInlineTest({
|
||||||
'a.test.ts': `
|
// 'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
// import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
// test('test', async ({ page }) => {
|
||||||
await page.setContent('<div>A</div>');
|
// await page.setContent('<div>A</div>');
|
||||||
const expectPromise = expect(page.locator('div')).toHaveText('A');
|
// test.step.skip('step', () => {});
|
||||||
expect(expectPromise instanceof Promise).toBeTruthy();
|
// await expect(page.locator('div')).toHaveText('A');
|
||||||
});
|
// });
|
||||||
`
|
// `
|
||||||
});
|
// });
|
||||||
expect(exitCode).toBe(0);
|
// expect(exitCode).toBe(0);
|
||||||
});
|
// const warnings = getWarnings(report);
|
||||||
|
// expect(warnings.length).toEqual(1);
|
||||||
|
// expect(warnings[0].description).toContain(warningSnippet);
|
||||||
|
// });
|
||||||
|
|
||||||
test('should warn about missing await in before hooks', async ({ runInlineTest }) => {
|
// test('traced promise should be instanceof Promise', async ({ runInlineTest }) => {
|
||||||
const group = ['beforeAll', 'beforeEach'];
|
// const { exitCode } = await runInlineTest({
|
||||||
for (const hook of group) {
|
// 'a.test.ts': `
|
||||||
await test.step(hook, async () => {
|
// import { test, expect } from '@playwright/test';
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// test('test', async ({ page }) => {
|
||||||
'a.test.ts': `
|
// await page.setContent('<div>A</div>');
|
||||||
import { test, expect } from '@playwright/test';
|
// const expectPromise = expect(page.locator('div')).toHaveText('A');
|
||||||
let page;
|
// expect(expectPromise instanceof Promise).toBeTruthy();
|
||||||
test.${hook}(async ({ browser }) => {
|
// });
|
||||||
page = await browser.newPage();
|
// `
|
||||||
await page.setContent('<div>A</div>');
|
// });
|
||||||
expect(page.locator('div')).toHaveText('A');
|
// expect(exitCode).toBe(0);
|
||||||
});
|
// });
|
||||||
test('test ${hook}', async () => {
|
|
||||||
await expect(page.locator('div')).toBeVisible();
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
// test('should warn about missing await in before hooks', async ({ runInlineTest }) => {
|
||||||
expect(stdout).toContain(warningSnippet);
|
// const group = ['beforeAll', 'beforeEach'];
|
||||||
expect(stdout).toContain(`${group[0]}/${group[1]} hooks`);
|
// for (const hook of group) {
|
||||||
});
|
// await test.step(hook, async () => {
|
||||||
}
|
// const { exitCode, report } = await runInlineTest({
|
||||||
});
|
// 'a.test.ts': `
|
||||||
|
// import { test, expect } from '@playwright/test';
|
||||||
|
// let page;
|
||||||
|
// test.${hook}(async ({ browser }) => {
|
||||||
|
// page = await browser.newPage();
|
||||||
|
// await page.setContent('<div>A</div>');
|
||||||
|
// expect(page.locator('div')).toHaveText('A');
|
||||||
|
// });
|
||||||
|
// test('test ${hook}', async () => {
|
||||||
|
// await expect(page.locator('div')).toBeVisible();
|
||||||
|
// });
|
||||||
|
// `
|
||||||
|
// });
|
||||||
|
|
||||||
test.describe('should warn about missing await in after hooks', () => {
|
// expect(exitCode).toBe(0);
|
||||||
const group = ['afterAll', 'afterEach'];
|
// const warnings = getWarnings(report);
|
||||||
for (const hook of group) {
|
// expect(warnings.length).toEqual(1);
|
||||||
test(hook, async ({ runInlineTest }) => {
|
// expect(warnings[0].description).toContain(warningSnippet);
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// expect(warnings[0].description).toContain(`${group[0]}/${group[1]} hooks`);
|
||||||
'a.test.ts': `
|
// });
|
||||||
import { test, expect } from '@playwright/test';
|
// }
|
||||||
let page;
|
// });
|
||||||
test('test ${hook}', async ({ browser }) => {
|
|
||||||
await expect(Promise.resolve()).resolves.toBe(undefined);
|
|
||||||
});
|
|
||||||
test.${hook}(async () => {
|
|
||||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
// test.describe('should warn about missing await in after hooks', () => {
|
||||||
expect(stdout).toContain(warningSnippet);
|
// const group = ['afterAll', 'afterEach'];
|
||||||
expect(stdout).toContain(`${group[0]}/${group[1]} hooks`);
|
// for (const hook of group) {
|
||||||
});
|
// test(hook, async ({ runInlineTest }) => {
|
||||||
}
|
// const { exitCode, report } = await runInlineTest({
|
||||||
});
|
// 'a.test.ts': `
|
||||||
|
// import { test, expect } from '@playwright/test';
|
||||||
|
// let page;
|
||||||
|
// test('test ${hook}', async ({ browser }) => {
|
||||||
|
// await expect(Promise.resolve()).resolves.toBe(undefined);
|
||||||
|
// });
|
||||||
|
// test.${hook}(async () => {
|
||||||
|
// expect(Promise.resolve()).resolves.toBe(undefined);
|
||||||
|
// });
|
||||||
|
// `
|
||||||
|
// });
|
||||||
|
|
||||||
test('should warn about missing await across hooks and test', async ({ runInlineTest }) => {
|
// expect(exitCode).toBe(0);
|
||||||
const { exitCode, stdout } = await runInlineTest({
|
// const warnings = getWarnings(report);
|
||||||
'a.test.ts': `
|
// expect(warnings.length).toEqual(1);
|
||||||
import { test, expect } from '@playwright/test';
|
// expect(warnings[0].description).toContain(warningSnippet);
|
||||||
test.beforeAll(async () => {
|
// expect(warnings[0].description).toContain(`${group[0]}/${group[1]} hooks`);
|
||||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
// });
|
||||||
});
|
// }
|
||||||
test('test', async () => {
|
// });
|
||||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
|
||||||
});
|
// test('should warn about missing await across hooks and test', async ({ runInlineTest }) => {
|
||||||
test.afterEach(async () => {
|
// const { exitCode, report } = await runInlineTest({
|
||||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
// 'a.test.ts': `
|
||||||
});
|
// import { test, expect } from '@playwright/test';
|
||||||
`
|
// test.beforeAll(async () => {
|
||||||
});
|
// expect(Promise.resolve()).resolves.toBe(undefined);
|
||||||
expect(exitCode).toBe(0);
|
// });
|
||||||
expect(stdout).toContain(`${warningSnippet} by the end of beforeAll/beforeEach hooks.`);
|
// test('test', async () => {
|
||||||
expect(stdout).toContain(`${warningSnippet} by the end of the test.`);
|
// expect(Promise.resolve()).resolves.toBe(undefined);
|
||||||
expect(stdout).toContain(`${warningSnippet} by the end of afterAll/afterEach hooks.`);
|
// });
|
||||||
});
|
// test.afterEach(async () => {
|
||||||
});
|
// expect(Promise.resolve()).resolves.toBe(undefined);
|
||||||
|
// });
|
||||||
|
// `
|
||||||
|
// });
|
||||||
|
// expect(exitCode).toBe(0);
|
||||||
|
// const warnings = getWarnings(report);
|
||||||
|
// expect(warnings.length).toEqual(3);
|
||||||
|
// expect(warnings[0].description).toContain(`${warningSnippet} by the end of beforeAll/beforeEach hooks.`);
|
||||||
|
// expect(warnings[1].description).toContain(`${warningSnippet} by the end of the test.`);
|
||||||
|
// expect(warnings[2].description).toContain(`${warningSnippet} by the end of afterAll/afterEach hooks.`);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue