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
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->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: |
|
||||
| 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-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequest.newContext.apiRequestFailsOnErrorStatus = %%-context-option-apiRequestFailsOnErrorStatus-%%
|
||||
### option: APIRequest.newContext.failOnStatusCode
|
||||
* 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-%%
|
||||
* since: v1.16
|
||||
|
|
@ -67,25 +71,7 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat
|
|||
- `localStorage` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` ?<[Array]<[Object]>> 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.
|
||||
- `indexedDB` ?<[Array]<[unknown]>> indexedDB to set for context
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -880,25 +880,7 @@ context cookies from the response. The method will automatically follow redirect
|
|||
- `localStorage` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` <[Array]<[Object]>>
|
||||
- `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.
|
||||
- `indexedDB` <[Array]<[unknown]>>
|
||||
|
||||
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
|
||||
- `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]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` <[Array]<[Object]>>
|
||||
- `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.
|
||||
- `indexedDB` <[Array]<[unknown]>>
|
||||
|
||||
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
|
||||
* since: v1.8
|
||||
* langs: csharp, java
|
||||
|
|
@ -1549,7 +1527,12 @@ IndexedDBs with typed arrays are currently not supported.
|
|||
* since: v1.51
|
||||
- `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
|
||||
* since: v1.12
|
||||
|
|
|
|||
|
|
@ -1090,6 +1090,9 @@ await rowLocator
|
|||
### option: Locator.filter.hasNotText = %%-locator-option-has-not-text-%%
|
||||
* since: v1.33
|
||||
|
||||
### option: Locator.filter.visible = %%-locator-option-visible-%%
|
||||
* since: v1.51
|
||||
|
||||
## method: Locator.first
|
||||
* since: v1.14
|
||||
- returns: <[Locator]>
|
||||
|
|
@ -2478,18 +2481,6 @@ When all steps combined have not finished during the specified [`option: timeout
|
|||
### option: Locator.uncheck.trial = %%-input-trial-%%
|
||||
* 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
|
||||
* 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).
|
||||
|
||||
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**
|
||||
|
||||
```js
|
||||
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
|
||||
|
|
|
|||
|
|
@ -296,7 +296,18 @@ Ensures the page is navigated to the given URL.
|
|||
**Usage**
|
||||
|
||||
```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
|
||||
|
|
@ -328,7 +339,7 @@ await Expect(Page).ToHaveURLAsync(new Regex(".*checkout"));
|
|||
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
|
||||
|
||||
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
|
||||
* 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
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` ?<[Array]<[Object]>> 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.
|
||||
- `indexedDB` ?<[Array]<[unknown]>> indexedDB to set for context
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
- `offline` <[boolean]>
|
||||
|
||||
|
|
@ -1001,7 +978,6 @@ between the same pixel in compared images, between zero (strict) and one (lax),
|
|||
- %%-context-option-locale-%%
|
||||
- %%-context-option-permissions-%%
|
||||
- %%-context-option-extrahttpheaders-%%
|
||||
- %%-context-option-apiRequestFailsOnErrorStatus-%%
|
||||
- %%-context-option-offline-%%
|
||||
- %%-context-option-httpcredentials-%%
|
||||
- %%-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.
|
||||
|
||||
## locator-option-visible
|
||||
- `visible` <[boolean]>
|
||||
|
||||
Only matches visible or invisible elements.
|
||||
|
||||
## locator-options-list-v1.14
|
||||
- %%-locator-option-has-text-%%
|
||||
- %%-locator-option-has-%%
|
||||
|
|
|
|||
|
|
@ -339,10 +339,10 @@ npx playwright test --update-snapshots --update-source-mode=3way
|
|||
|
||||
#### 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
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -751,10 +751,10 @@ page.locator("x-details", new Page.LocatorOptions().setHasText("Details"))
|
|||
.click();
|
||||
```
|
||||
```python async
|
||||
await page.locator("x-details", has_text="Details" ).click()
|
||||
await page.locator("x-details", has_text="Details").click()
|
||||
```
|
||||
```python sync
|
||||
page.locator("x-details", has_text="Details" ).click()
|
||||
page.locator("x-details", has_text="Details").click()
|
||||
```
|
||||
```csharp
|
||||
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.
|
||||
|
||||
```js
|
||||
await page.locator('button').visible().click();
|
||||
await page.locator('button').filter({ visible: true }).click();
|
||||
```
|
||||
```java
|
||||
page.locator("button").visible().click();
|
||||
page.locator("button").filter(new Locator.FilterOptions.setVisible(true)).click();
|
||||
```
|
||||
```python async
|
||||
await page.locator("button").visible().click()
|
||||
await page.locator("button").filter(visible=True).click()
|
||||
```
|
||||
```python sync
|
||||
page.locator("button").visible().click()
|
||||
page.locator("button").filter(visible=True).click()
|
||||
```
|
||||
```csharp
|
||||
await page.Locator("button").Visible().ClickAsync();
|
||||
await page.Locator("button").Filter(new() { Visible = true }).ClickAsync();
|
||||
```
|
||||
|
||||
## Lists
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ See [`property: TestConfig.reporter`].
|
|||
## property: FullConfig.reportSlowTests
|
||||
* since: v1.10
|
||||
- type: <[null]|[Object]>
|
||||
- `max` <[int]> The maximum number of slow test files to report. Defaults to `5`.
|
||||
- `threshold` <[float]> Test duration in milliseconds that is considered slow. Defaults to 15 seconds.
|
||||
- `max` <[int]> The maximum number of slow test files to report.
|
||||
- `threshold` <[float]> Test file duration in milliseconds that is considered slow.
|
||||
|
||||
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.
|
||||
|
||||
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**
|
||||
|
||||
|
|
@ -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-%%
|
||||
* 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
|
||||
* since: v1.10
|
||||
- type: ?<[PreserveOutput]<"always"|"never"|"failures-only">>
|
||||
|
|
@ -446,7 +429,7 @@ export default defineConfig({
|
|||
* since: v1.10
|
||||
- type: ?<[null]|[Object]>
|
||||
- `max` <[int]> The maximum number of slow test files to report. Defaults to `5`.
|
||||
- `threshold` <[float]> Test duration in milliseconds that is considered slow. Defaults to 15 seconds.
|
||||
- `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.
|
||||
|
||||
|
|
@ -680,7 +663,7 @@ import { defineConfig } from '@playwright/test';
|
|||
export default defineConfig({
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
url: 'http://localhost:3000',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
|
@ -709,19 +692,19 @@ export default defineConfig({
|
|||
webServer: [
|
||||
{
|
||||
command: 'npm run start',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
url: 'http://localhost:3000',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
command: 'npm run backend',
|
||||
url: 'http://127.0.0.1:3333',
|
||||
url: 'http://localhost:3333',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
}
|
||||
],
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:3000',
|
||||
baseURL: 'http://localhost:3000',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export default defineConfig({
|
|||
|
||||
use: {
|
||||
// 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.
|
||||
trace: 'on-first-retry',
|
||||
|
|
@ -50,7 +50,7 @@ export default defineConfig({
|
|||
// Run your local dev server before starting the tests.
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { defineConfig } from '@playwright/test';
|
|||
export default defineConfig({
|
||||
use: {
|
||||
// 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.
|
||||
storageState: 'state.json',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export default defineConfig({
|
|||
// Run your local dev server before starting the tests
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: 'ignore',
|
||||
stderr: 'pipe',
|
||||
|
|
@ -52,7 +52,7 @@ export default defineConfig({
|
|||
// Run your local dev server before starting the tests
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
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.
|
||||
|
||||
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"
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
|
@ -74,11 +74,11 @@ export default defineConfig({
|
|||
// Run your local dev server before starting the tests
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
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';
|
||||
|
||||
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');
|
||||
});
|
||||
```
|
||||
|
|
@ -106,19 +106,19 @@ export default defineConfig({
|
|||
webServer: [
|
||||
{
|
||||
command: 'npm run start',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
url: 'http://localhost:3000',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
command: 'npm run backend',
|
||||
url: 'http://127.0.0.1:3333',
|
||||
url: 'http://localhost:3333',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
}
|
||||
],
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:3000',
|
||||
baseURL: 'http://localhost:3000',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -253,7 +253,11 @@ export default [{
|
|||
'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,
|
||||
rules: {
|
||||
...noWebGlobalsRules,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@ export default defineConfig({
|
|||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
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: {
|
||||
ctPort: 3101,
|
||||
ctViteConfig: {
|
||||
|
|
|
|||
|
|
@ -37,10 +37,6 @@
|
|||
line-height: 24px;
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata-properties {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -57,9 +53,8 @@
|
|||
border-bottom: 1px solid var(--color-border-default);
|
||||
}
|
||||
|
||||
.git-commit-info a {
|
||||
.metadata-view a {
|
||||
color: var(--color-fg-default);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.copyable-property {
|
||||
|
|
|
|||
|
|
@ -20,32 +20,10 @@ import './common.css';
|
|||
import './theme.css';
|
||||
import './metadataView.css';
|
||||
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 { 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 }> {
|
||||
override state: { error: Error | null, errorInfo: React.ErrorInfo | null } = {
|
||||
error: null,
|
||||
|
|
@ -72,27 +50,26 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
|||
}
|
||||
}
|
||||
|
||||
export const MetadataView = () => {
|
||||
return <ErrorBoundary><InnerMetadataView/></ErrorBoundary>;
|
||||
export const MetadataView: React.FC<{ metadata: Metadata }> = params => {
|
||||
return <ErrorBoundary><InnerMetadataView metadata={params.metadata}/></ErrorBoundary>;
|
||||
};
|
||||
|
||||
const InnerMetadataView = () => {
|
||||
const metadataEntries = useMetadata();
|
||||
const gitCommitInfo = useGitCommitInfo();
|
||||
const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
|
||||
if (!gitCommitInfo && !entries.length)
|
||||
return null;
|
||||
const InnerMetadataView: React.FC<{ metadata: Metadata }> = params => {
|
||||
const commitInfo = params.metadata as MetadataWithCommitInfo;
|
||||
const otherEntries = Object.entries(params.metadata).filter(([key]) => !ignoreKeys.has(key));
|
||||
const hasMetadata = commitInfo.ci || commitInfo.gitCommit || otherEntries.length > 0;
|
||||
if (!hasMetadata)
|
||||
return;
|
||||
return <div className='metadata-view'>
|
||||
{gitCommitInfo && <>
|
||||
<GitCommitInfoView info={gitCommitInfo}/>
|
||||
{entries.length > 0 && <div className='metadata-separator' />}
|
||||
</>}
|
||||
<div className='metadata-section metadata-properties'>
|
||||
{entries.map(([propertyName, value]) => {
|
||||
{commitInfo.ci && !commitInfo.gitCommit && <CiInfoView info={commitInfo.ci}/>}
|
||||
{commitInfo.gitCommit && <GitCommitInfoView ci={commitInfo.ci} commit={commitInfo.gitCommit}/>}
|
||||
{otherEntries.length > 0 && (commitInfo.gitCommit || commitInfo.ci) && <div className='metadata-separator' />}
|
||||
<div className='metadata-section metadata-properties' role='list'>
|
||||
{otherEntries.map(([propertyName, 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;
|
||||
return (
|
||||
<div key={propertyName} className='copyable-property'>
|
||||
<div key={propertyName} className='copyable-property' role='listitem'>
|
||||
<CopyToClipboardContainer value={valueString}>
|
||||
<span style={{ fontWeight: 'bold' }} title={propertyName}>{propertyName}</span>
|
||||
: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span>
|
||||
|
|
@ -104,48 +81,39 @@ const InnerMetadataView = () => {
|
|||
</div>;
|
||||
};
|
||||
|
||||
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
||||
const email = info['revision.email'] ? ` <${info['revision.email']}>` : '';
|
||||
const author = `${info['revision.author'] || ''}${email}`;
|
||||
|
||||
let subject = info['revision.subject'] || '';
|
||||
let link = info['revision.link'];
|
||||
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>
|
||||
const CiInfoView: React.FC<{ info: CIInfo }> = ({ info }) => {
|
||||
const title = info.prTitle || `Commit ${info.commitHash}`;
|
||||
const link = info.prHref || info.commitHref;
|
||||
return <div className='metadata-section' role='list'>
|
||||
<div role='listitem'>
|
||||
<a href={link} target='_blank' rel='noopener noreferrer' title={title}>{title}</a>
|
||||
</div>
|
||||
{link ? (
|
||||
<a href={link} target='_blank' rel='noopener noreferrer' title='View commit details'>
|
||||
{shortSubject}
|
||||
</a>
|
||||
) : !!shortSubject && <span>{shortSubject}</span>}
|
||||
</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 { TestFilesHeader, TestFilesView } from './testFilesView';
|
||||
import './theme.css';
|
||||
import { MetadataProvider } from './metadataView';
|
||||
import { HTMLReportContextProvider } from './reportContext';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -73,7 +73,7 @@ export const ReportView: React.FC<{
|
|||
return result;
|
||||
}, [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>
|
||||
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||
<Route predicate={testFilesRoutePredicate}>
|
||||
|
|
@ -89,7 +89,7 @@ export const ReportView: React.FC<{
|
|||
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
|
||||
</Route>
|
||||
</main>
|
||||
</div></MetadataProvider>;
|
||||
</div></HTMLReportContextProvider>;
|
||||
};
|
||||
|
||||
const TestCaseViewLoader: React.FC<{
|
||||
|
|
|
|||
|
|
@ -21,9 +21,14 @@ import type { ImageDiff } from '@web/shared/imageDiffView';
|
|||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import type { TestResult } from './types';
|
||||
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 (
|
||||
<CodeSnippet code={error} testId={testId}>
|
||||
<div style={{ float: 'right', margin: 10 }}>
|
||||
|
|
@ -47,12 +52,13 @@ const PromptButton: React.FC<{
|
|||
error: string;
|
||||
result?: TestResult;
|
||||
}> = ({ error, result }) => {
|
||||
const gitCommitInfo = useGitCommitInfo();
|
||||
const report = useHTMLReport();
|
||||
const commitInfo = report?.metadata as MetadataWithCommitInfo | undefined;
|
||||
const prompt = React.useMemo(() => fixTestPrompt(
|
||||
error,
|
||||
gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'],
|
||||
commitInfo?.gitDiff,
|
||||
result?.attachments.find(a => a.name === 'pageSnapshot')?.body
|
||||
), [gitCommitInfo, result, error]);
|
||||
), [commitInfo, result, error]);
|
||||
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { msToString } from './utils';
|
|||
import { AutoChip } from './chip';
|
||||
import { TestErrorView } from './testErrorView';
|
||||
import * as icons from './icons';
|
||||
import { MetadataView, useMetadata } from './metadataView';
|
||||
import { isMetadataEmpty, MetadataView } from './metadataView';
|
||||
|
||||
export const TestFilesView: React.FC<{
|
||||
tests: TestFileSummary[],
|
||||
|
|
@ -67,13 +67,12 @@ export const TestFilesHeader: React.FC<{
|
|||
metadataVisible: boolean,
|
||||
toggleMetadataVisible: () => void,
|
||||
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
|
||||
const metadataEntries = useMetadata();
|
||||
if (!report)
|
||||
return null;
|
||||
return <>
|
||||
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
|
||||
<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
|
||||
</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-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
|
||||
</div>
|
||||
{metadataVisible && <MetadataView/>}
|
||||
{metadataVisible && <MetadataView metadata={report.metadata}/>}
|
||||
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||
</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
|
||||
* snapshot.
|
||||
*
|
||||
* **NOTE** IndexedDBs with typed arrays are currently not supported.
|
||||
*
|
||||
* @param 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;
|
||||
|
||||
|
|
@ -9316,49 +9317,7 @@ export interface BrowserContext {
|
|||
value: string;
|
||||
}>;
|
||||
|
||||
indexedDB: Array<{
|
||||
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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB: Array<unknown>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -9741,12 +9700,6 @@ export interface Browser {
|
|||
*/
|
||||
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),
|
||||
* [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?: Array<{
|
||||
/**
|
||||
* 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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB?: Array<unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -13224,6 +13129,11 @@ export interface Locator {
|
|||
* `<article><div>Playwright</div></article>`.
|
||||
*/
|
||||
hasText?: string|RegExp;
|
||||
|
||||
/**
|
||||
* Only matches visible or invisible elements.
|
||||
*/
|
||||
visible?: boolean;
|
||||
}): Locator;
|
||||
|
||||
/**
|
||||
|
|
@ -14615,17 +14525,6 @@ export interface Locator {
|
|||
trial?: boolean;
|
||||
}): 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
|
||||
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.
|
||||
|
|
@ -14824,12 +14723,6 @@ export interface BrowserType<Unused = {}> {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
@ -16720,12 +16613,6 @@ export interface AndroidDevice {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
@ -17572,12 +17459,6 @@ export interface APIRequest {
|
|||
* @param 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
|
||||
* [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; };
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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?: Array<{
|
||||
/**
|
||||
* 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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB?: Array<unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -18576,7 +18415,7 @@ export interface APIRequestContext {
|
|||
*/
|
||||
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;
|
||||
|
||||
|
|
@ -18618,49 +18457,7 @@ export interface APIRequestContext {
|
|||
value: string;
|
||||
}>;
|
||||
|
||||
indexedDB: Array<{
|
||||
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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB: Array<unknown>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -22157,12 +21954,6 @@ export interface BrowserContextOptions {
|
|||
*/
|
||||
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),
|
||||
* [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?: Array<{
|
||||
/**
|
||||
* 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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB?: Array<unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1160",
|
||||
"revision": "1161",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "134.0.6998.23"
|
||||
"browserVersion": "134.0.6998.35"
|
||||
},
|
||||
{
|
||||
"name": "chromium-headless-shell",
|
||||
"revision": "1160",
|
||||
"revision": "1161",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "134.0.6998.23"
|
||||
"browserVersion": "134.0.6998.35"
|
||||
},
|
||||
{
|
||||
"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']> {
|
||||
if (typeof options.storageState !== 'string')
|
||||
return options.storageState;
|
||||
return options.storageState as any;
|
||||
try {
|
||||
return JSON.parse(await platform.fs().promises.readFile(options.storageState, 'utf8'));
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export type LocatorOptions = {
|
|||
hasNotText?: string | RegExp;
|
||||
has?: Locator;
|
||||
hasNot?: Locator;
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (options?.visible !== undefined)
|
||||
this._selector += ` >> visible=${options.visible ? 'true' : 'false'}`;
|
||||
|
||||
if (this._frame._platform.inspectCustom)
|
||||
(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);
|
||||
}
|
||||
|
||||
locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator {
|
||||
locator(selectorOrLocator: string | Locator, options?: Omit<LocatorOptions, 'visible'>): Locator {
|
||||
if (isString(selectorOrLocator))
|
||||
return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options);
|
||||
if (selectorOrLocator._frame !== this._frame)
|
||||
|
|
@ -218,11 +222,6 @@ export class Locator implements api.Locator {
|
|||
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 {
|
||||
if (locator._frame !== this._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 StorageState = {
|
||||
cookies: channels.NetworkCookie[],
|
||||
origins: channels.OriginStorage[],
|
||||
origins: (Omit<channels.OriginStorage, 'indexedDB'> & { indexedDB: unknown[] })[],
|
||||
};
|
||||
export type SetStorageState = {
|
||||
cookies?: channels.SetNetworkCookie[],
|
||||
origins?: channels.SetOriginStorage[]
|
||||
origins?: (Omit<channels.SetOriginStorage, 'indexedDB'> & { indexedDB?: unknown[] })[]
|
||||
};
|
||||
|
||||
export type LifecycleEvent = channels.LifecycleEvent;
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ scheme.PlaywrightNewRequestParams = tObject({
|
|||
userAgent: tOptional(tString),
|
||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
||||
failOnStatusCode: tOptional(tBoolean),
|
||||
clientCertificates: tOptional(tArray(tObject({
|
||||
origin: tString,
|
||||
cert: tOptional(tBinary),
|
||||
|
|
@ -600,7 +600,6 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
|
|||
})),
|
||||
permissions: tOptional(tArray(tString)),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
||||
offline: tOptional(tBoolean),
|
||||
httpCredentials: tOptional(tObject({
|
||||
username: tString,
|
||||
|
|
@ -688,7 +687,6 @@ scheme.BrowserNewContextParams = tObject({
|
|||
})),
|
||||
permissions: tOptional(tArray(tString)),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
||||
offline: tOptional(tBoolean),
|
||||
httpCredentials: tOptional(tObject({
|
||||
username: tString,
|
||||
|
|
@ -759,7 +757,6 @@ scheme.BrowserNewContextForReuseParams = tObject({
|
|||
})),
|
||||
permissions: tOptional(tArray(tString)),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
||||
offline: tOptional(tBoolean),
|
||||
httpCredentials: tOptional(tObject({
|
||||
username: tString,
|
||||
|
|
@ -2667,7 +2664,6 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
|
|||
})),
|
||||
permissions: tOptional(tArray(tString)),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
||||
offline: tOptional(tBoolean),
|
||||
httpCredentials: tOptional(tObject({
|
||||
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> {
|
||||
// Bidi throws when x/y are not integers.
|
||||
x = Math.floor(x);
|
||||
y = Math.floor(y);
|
||||
await this._performActions([{ type: 'pointerMove', x, y }]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -511,7 +511,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
this._origins.add(origin);
|
||||
}
|
||||
|
||||
async storageState(indexedDB = true): Promise<channels.BrowserContextStorageStateResult> {
|
||||
async storageState(indexedDB = false): Promise<channels.BrowserContextStorageStateResult> {
|
||||
const result: channels.BrowserContextStorageStateResult = {
|
||||
cookies: await this.cookies(),
|
||||
origins: []
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy S5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 740
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 740,
|
||||
"height": 360
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 320,
|
||||
"height": 658
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+ landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 658,
|
||||
"height": 320
|
||||
|
|
@ -176,7 +176,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 712,
|
||||
"height": 1138
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1138,
|
||||
"height": 712
|
||||
|
|
@ -1098,7 +1098,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"LG Optimus L70": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/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": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1109,7 +1109,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"LG Optimus L70 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1120,7 +1120,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 550": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1131,7 +1131,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 550 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1142,7 +1142,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 950": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1153,7 +1153,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 950 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1164,7 +1164,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 800,
|
||||
"height": 1280
|
||||
|
|
@ -1175,7 +1175,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
|
|
@ -1186,7 +1186,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1197,7 +1197,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1208,7 +1208,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1219,7 +1219,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1230,7 +1230,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1241,7 +1241,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1252,7 +1252,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1263,7 +1263,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1274,7 +1274,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1285,7 +1285,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1296,7 +1296,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 600,
|
||||
"height": 960
|
||||
|
|
@ -1307,7 +1307,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 960,
|
||||
"height": 600
|
||||
|
|
@ -1362,7 +1362,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Pixel 2": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 411,
|
||||
"height": 731
|
||||
|
|
@ -1373,7 +1373,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 731,
|
||||
"height": 411
|
||||
|
|
@ -1384,7 +1384,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 411,
|
||||
"height": 823
|
||||
|
|
@ -1395,7 +1395,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 823,
|
||||
"height": 411
|
||||
|
|
@ -1406,7 +1406,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 393,
|
||||
"height": 786
|
||||
|
|
@ -1417,7 +1417,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 786,
|
||||
"height": 393
|
||||
|
|
@ -1428,7 +1428,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 353,
|
||||
"height": 745
|
||||
|
|
@ -1439,7 +1439,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 745,
|
||||
"height": 353
|
||||
|
|
@ -1450,7 +1450,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G)": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 892
|
||||
|
|
@ -1465,7 +1465,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G) landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"height": 892,
|
||||
"width": 412
|
||||
|
|
@ -1480,7 +1480,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 393,
|
||||
"height": 851
|
||||
|
|
@ -1495,7 +1495,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 851,
|
||||
"height": 393
|
||||
|
|
@ -1510,7 +1510,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 915
|
||||
|
|
@ -1525,7 +1525,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 915,
|
||||
"height": 412
|
||||
|
|
@ -1540,7 +1540,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1551,7 +1551,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1562,7 +1562,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Chrome HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1577,7 +1577,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1622,7 +1622,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Desktop Chrome": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1637,7 +1637,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ import type { Readable, TransformCallback } from 'stream';
|
|||
type FetchRequestOptions = {
|
||||
userAgent: string;
|
||||
extraHTTPHeaders?: HeadersArray;
|
||||
apiRequestFailsOnErrorStatus?: boolean;
|
||||
failOnStatusCode?: boolean;
|
||||
httpCredentials?: HTTPCredentials;
|
||||
proxy?: ProxySettings;
|
||||
timeoutSettings: TimeoutSettings;
|
||||
|
|
@ -212,7 +212,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
});
|
||||
const fetchUid = this._storeResponseBody(fetchResponse.body);
|
||||
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)) {
|
||||
let responseText = '';
|
||||
if (fetchResponse.body.byteLength) {
|
||||
|
|
@ -608,7 +608,7 @@ export class BrowserContextAPIRequestContext extends APIRequestContext {
|
|||
return {
|
||||
userAgent: this._context._options.userAgent || this._context._browser.userAgent(),
|
||||
extraHTTPHeaders: this._context._options.extraHTTPHeaders,
|
||||
apiRequestFailsOnErrorStatus: this._context._options.apiRequestFailsOnErrorStatus,
|
||||
failOnStatusCode: undefined,
|
||||
httpCredentials: this._context._options.httpCredentials,
|
||||
proxy: this._context._options.proxy || this._context._browser.options.proxy,
|
||||
timeoutSettings: this._context._timeoutSettings,
|
||||
|
|
@ -660,7 +660,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
|||
baseURL: options.baseURL,
|
||||
userAgent: options.userAgent || getUserAgent(),
|
||||
extraHTTPHeaders: options.extraHTTPHeaders,
|
||||
apiRequestFailsOnErrorStatus: !!options.apiRequestFailsOnErrorStatus,
|
||||
failOnStatusCode: !!options.failOnStatusCode,
|
||||
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
|
||||
httpCredentials: options.httpCredentials,
|
||||
clientCertificates: options.clientCertificates,
|
||||
|
|
@ -693,7 +693,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
|||
return this._cookieStore.cookies(url);
|
||||
}
|
||||
|
||||
override async storageState(indexedDB = true): Promise<channels.APIRequestContextStorageStateResult> {
|
||||
override async storageState(indexedDB = false): Promise<channels.APIRequestContextStorageStateResult> {
|
||||
return {
|
||||
cookies: this._cookieStore.allCookies(),
|
||||
origins: (this._origins || []).map(origin => ({ ...origin, indexedDB: indexedDB ? origin.indexedDB : [] })),
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class Locator {
|
|||
element: 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)
|
||||
selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
|
||||
if (options?.hasNotText)
|
||||
|
|
@ -38,6 +38,8 @@ class Locator {
|
|||
selector += ` >> internal:has=` + JSON.stringify(options.has[selectorSymbol]);
|
||||
if (options?.hasNot)
|
||||
selector += ` >> internal:has-not=` + JSON.stringify(options.hasNot[selectorSymbol]);
|
||||
if (options?.visible !== undefined)
|
||||
selector += ` >> visible=${options.visible ? 'true' : 'false'}`;
|
||||
this[selectorSymbol] = selector;
|
||||
if (selector) {
|
||||
const parsed = injectedScript.parseSelector(selector);
|
||||
|
|
@ -46,7 +48,7 @@ class Locator {
|
|||
}
|
||||
const selectorBase = selector;
|
||||
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);
|
||||
};
|
||||
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.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.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.last = (): Locator => self.locator('nth=-1');
|
||||
self.nth = (index: number): Locator => self.locator(`nth=${index}`);
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ function trimFlatString(s: string): string {
|
|||
function asFlatString(s: string): string {
|
||||
// "Flat string" at https://w3c.github.io/accname/#terminology
|
||||
// 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[] {
|
||||
|
|
|
|||
|
|
@ -129,10 +129,13 @@ export function source() {
|
|||
|
||||
function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
|
||||
if (value && typeof value === 'object') {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (typeof globalThis.Window === 'function' && value instanceof globalThis.Window)
|
||||
return 'ref: <Window>';
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (typeof globalThis.Document === 'function' && value instanceof globalThis.Document)
|
||||
return 'ref: <Document>';
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (typeof globalThis.Node === 'function' && value instanceof globalThis.Node)
|
||||
return 'ref: <Node>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,7 +220,8 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: yaml
|
|||
const emptyFragment: AriaTemplateRoleNode = { kind: 'role', role: 'fragment' };
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||
case 'last':
|
||||
return `last()`;
|
||||
case 'visible':
|
||||
return `visible(${body === 'true' ? '' : '{ visible: false }'})`;
|
||||
return `filter({ visible: ${body === 'true' ? 'true' : 'false'} })`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
if (isRegExp(options.name)) {
|
||||
|
|
@ -376,7 +376,7 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
case 'last':
|
||||
return `last`;
|
||||
case 'visible':
|
||||
return `visible(${body === 'true' ? '' : 'visible=False'})`;
|
||||
return `filter(visible=${body === 'true' ? 'True' : 'False'})`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
if (isRegExp(options.name)) {
|
||||
|
|
@ -485,7 +485,7 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
case 'last':
|
||||
return `last()`;
|
||||
case 'visible':
|
||||
return `visible(${body === 'true' ? '' : `new ${clazz}.VisibleOptions().setVisible(false)`})`;
|
||||
return `filter(new ${clazz}.FilterOptions().setVisible(${body === 'true' ? 'true' : 'false'}))`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
if (isRegExp(options.name)) {
|
||||
|
|
@ -584,7 +584,7 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||
case 'last':
|
||||
return `Last`;
|
||||
case 'visible':
|
||||
return `Visible(${body === 'true' ? '' : 'new() { Visible = false }'})`;
|
||||
return `Filter(new() { Visible = ${body === 'true' ? 'true' : 'false'} })`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
if (isRegExp(options.name)) {
|
||||
|
|
|
|||
|
|
@ -170,9 +170,8 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
|||
.replace(/first(\(\))?/g, 'nth=0')
|
||||
.replace(/last(\(\))?/g, 'nth=-1')
|
||||
.replace(/nth\(([^)]+)\)/g, 'nth=$1')
|
||||
.replace(/visible\(,?visible=true\)/g, 'visible=true')
|
||||
.replace(/visible\(,?visible=false\)/g, 'visible=false')
|
||||
.replace(/visible\(\)/g, 'visible=true')
|
||||
.replace(/filter\(,?visible=true\)/g, 'visible=true')
|
||||
.replace(/filter\(,?visible=false\)/g, 'visible=false')
|
||||
.replace(/filter\(,?hastext=([^)]+)\)/g, 'internal:has-text=$1')
|
||||
.replace(/filter\(,?hasnottext=([^)]+)\)/g, 'internal:has-not-text=$1')
|
||||
.replace(/filter\(,?has2=([^)]+)\)/g, 'internal:has=$1')
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export function cacheNormalizedWhitespaces() {
|
|||
export function normalizeWhiteSpace(text: string): string {
|
||||
let result = normalizedWhitespaceCache?.get(text);
|
||||
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);
|
||||
}
|
||||
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
|
||||
* snapshot.
|
||||
*
|
||||
* **NOTE** IndexedDBs with typed arrays are currently not supported.
|
||||
*
|
||||
* @param 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;
|
||||
|
||||
|
|
@ -9316,49 +9317,7 @@ export interface BrowserContext {
|
|||
value: string;
|
||||
}>;
|
||||
|
||||
indexedDB: Array<{
|
||||
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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB: Array<unknown>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -9741,12 +9700,6 @@ export interface Browser {
|
|||
*/
|
||||
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),
|
||||
* [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?: Array<{
|
||||
/**
|
||||
* 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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB?: Array<unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -13224,6 +13129,11 @@ export interface Locator {
|
|||
* `<article><div>Playwright</div></article>`.
|
||||
*/
|
||||
hasText?: string|RegExp;
|
||||
|
||||
/**
|
||||
* Only matches visible or invisible elements.
|
||||
*/
|
||||
visible?: boolean;
|
||||
}): Locator;
|
||||
|
||||
/**
|
||||
|
|
@ -14615,17 +14525,6 @@ export interface Locator {
|
|||
trial?: boolean;
|
||||
}): 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
|
||||
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.
|
||||
|
|
@ -14824,12 +14723,6 @@ export interface BrowserType<Unused = {}> {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
@ -16720,12 +16613,6 @@ export interface AndroidDevice {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
@ -17572,12 +17459,6 @@ export interface APIRequest {
|
|||
* @param 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
|
||||
* [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; };
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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?: Array<{
|
||||
/**
|
||||
* 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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB?: Array<unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -18576,7 +18415,7 @@ export interface APIRequestContext {
|
|||
*/
|
||||
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;
|
||||
|
||||
|
|
@ -18618,49 +18457,7 @@ export interface APIRequestContext {
|
|||
value: string;
|
||||
}>;
|
||||
|
||||
indexedDB: Array<{
|
||||
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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB: Array<unknown>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -22157,12 +21954,6 @@ export interface BrowserContextOptions {
|
|||
*/
|
||||
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),
|
||||
* [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?: Array<{
|
||||
/**
|
||||
* 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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
indexedDB?: Array<unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ export class FullConfigInternal {
|
|||
readonly plugins: TestRunnerPluginRegistration[];
|
||||
readonly projects: FullProjectInternal[] = [];
|
||||
readonly singleTSConfigPath?: string;
|
||||
readonly populateGitInfo: boolean;
|
||||
cliArgs: string[] = [];
|
||||
cliGrep: string | undefined;
|
||||
cliGrepInvert: string | undefined;
|
||||
|
|
@ -78,7 +77,6 @@ export class FullConfigInternal {
|
|||
const privateConfiguration = (userConfig as any)['@playwright/test'];
|
||||
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
|
||||
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.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),
|
||||
globalSetup: this.globalSetups[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),
|
||||
grepInvert: takeFirst(userConfig.grepInvert, null),
|
||||
maxFailures: takeFirst(configCLIOverrides.debug ? 1 : undefined, configCLIOverrides.maxFailures, userConfig.maxFailures, 0),
|
||||
metadata: userConfig.metadata,
|
||||
preserveOutput: takeFirst(userConfig.preserveOutput, 'always'),
|
||||
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),
|
||||
projects: [],
|
||||
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 defaultReporter = process.env.CI ? 'dot' : 'list';
|
||||
const defaultPopulateGitInfo = process.env.GITHUB_ACTIONS === 'true';
|
||||
|
||||
const configInternalSymbol = Symbol('configInternalSymbol');
|
||||
|
||||
|
|
|
|||
|
|
@ -628,7 +628,7 @@ class ArtifactsRecorder {
|
|||
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 });
|
||||
await fs.promises.writeFile(path, ariaSnapshot);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -605,7 +605,7 @@ export const baseFullConfig: reporterTypes.FullConfig = {
|
|||
preserveOutput: 'always',
|
||||
projects: [],
|
||||
reporter: [[process.env.CI ? 'dot' : 'list']],
|
||||
reportSlowTests: { max: 5, threshold: 15000 },
|
||||
reportSlowTests: { max: 5, threshold: 300_000 /* 5 minutes */ },
|
||||
configFile: '',
|
||||
rootDir: '',
|
||||
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.
|
||||
*/
|
||||
|
||||
export interface GitCommitInfo {
|
||||
'revision.id'?: string;
|
||||
'revision.author'?: string;
|
||||
'revision.email'?: string;
|
||||
'revision.subject'?: string;
|
||||
'revision.timestamp'?: number | Date;
|
||||
'revision.link'?: string;
|
||||
'revision.diff'?: string;
|
||||
'pull.link'?: string;
|
||||
'pull.diff'?: string;
|
||||
'pull.base'?: string;
|
||||
'pull.title'?: string;
|
||||
'ci.link'?: string;
|
||||
}
|
||||
export type GitCommitInfo = {
|
||||
shortHash: string;
|
||||
hash: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
time: number;
|
||||
};
|
||||
committer: {
|
||||
name: 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 { EXPECTED_COLOR } from '../common/expectBundle';
|
||||
import { callLogText, sanitizeFilePathBeforeExtension, trimLongString } from '../util';
|
||||
import { callLogText, fileExistsAsync, sanitizeFilePathBeforeExtension, trimLongString } from '../util';
|
||||
import { printReceivedStringContainExpectedSubstring } from './expect';
|
||||
import { currentTestInfo } from '../common/globals';
|
||||
|
||||
|
|
@ -70,7 +70,8 @@ export async function toMatchAriaSnapshot(
|
|||
timeout = options.timeout ?? this.timeout;
|
||||
} else {
|
||||
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 {
|
||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||
if (!snapshotNames) {
|
||||
|
|
@ -78,7 +79,14 @@ export async function toMatchAriaSnapshot(
|
|||
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
||||
}
|
||||
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(() => '');
|
||||
timeout = expectedParam?.timeout ?? this.timeout;
|
||||
|
|
|
|||
|
|
@ -14,108 +14,170 @@
|
|||
* 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 { FullConfig } from '../../types/testReporter';
|
||||
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) => {
|
||||
if (fullConfig.populateGitInfo)
|
||||
fullConfig.plugins.push({ factory: gitCommitInfo });
|
||||
fullConfig.plugins.push({ factory: gitCommitInfoPlugin });
|
||||
};
|
||||
|
||||
export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
|
||||
type GitCommitInfoPluginOptions = {
|
||||
directory?: string;
|
||||
};
|
||||
|
||||
export const gitCommitInfoPlugin = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
|
||||
return {
|
||||
name: 'playwright:git-commit-info',
|
||||
|
||||
setup: async (config: FullConfig, configDir: string) => {
|
||||
const fromEnv = await linksFromEnv();
|
||||
const fromCLI = await gitStatusFromCLI(options?.directory || configDir, fromEnv);
|
||||
config.metadata = config.metadata || {};
|
||||
config.metadata['git.commit.info'] = { ...fromEnv, ...fromCLI };
|
||||
const metadata = config.metadata as UserMetadataWithCommitInfo;
|
||||
const ci = await ciInfo();
|
||||
if (!metadata.ci && ci)
|
||||
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 {
|
||||
directory?: string;
|
||||
}
|
||||
|
||||
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) {
|
||||
async function ciInfo(): Promise<CIInfo | undefined> {
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
let pr: { title: string, number: number } | undefined;
|
||||
try {
|
||||
const json = JSON.parse(await fs.promises.readFile(process.env.GITHUB_EVENT_PATH, 'utf8'));
|
||||
if (json.pull_request) {
|
||||
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;
|
||||
}
|
||||
const json = JSON.parse(await fs.promises.readFile(process.env.GITHUB_EVENT_PATH!, 'utf8'));
|
||||
pr = { title: json.pull_request.title, number: json.pull_request.number };
|
||||
} 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> {
|
||||
const separator = `:${createGuid().slice(0, 4)}:`;
|
||||
const commitInfoResult = await spawnAsync(
|
||||
'git',
|
||||
['show', '-s', `--format=%H${separator}%s${separator}%an${separator}%ae${separator}%ct`, 'HEAD'],
|
||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||
);
|
||||
if (commitInfoResult.code)
|
||||
return;
|
||||
const showOutput = commitInfoResult.stdout.trim();
|
||||
const [id, subject, author, email, rawTimestamp] = showOutput.split(separator);
|
||||
let timestamp: number = Number.parseInt(rawTimestamp, 10);
|
||||
timestamp = Number.isInteger(timestamp) ? timestamp * 1000 : 0;
|
||||
async function gitCommitInfo(gitDir: string): Promise<GitCommitInfo | undefined> {
|
||||
const separator = `---786eec917292---`;
|
||||
const tokens = [
|
||||
'%H', // commit hash
|
||||
'%h', // abbreviated commit hash
|
||||
'%s', // subject
|
||||
'%B', // raw body (unwrapped subject and body)
|
||||
'%an', // author name
|
||||
'%ae', // author email
|
||||
'%at', // author date, UNIX timestamp
|
||||
'%cn', // committer name
|
||||
'%ce', // committer email
|
||||
'%ct', // committer date, UNIX timestamp
|
||||
'', // branch
|
||||
];
|
||||
const 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 = {
|
||||
'revision.id': id,
|
||||
'revision.author': author,
|
||||
'revision.email': email,
|
||||
'revision.subject': subject,
|
||||
'revision.timestamp': timestamp,
|
||||
return {
|
||||
shortHash,
|
||||
hash,
|
||||
subject,
|
||||
body,
|
||||
author: {
|
||||
name: authorName,
|
||||
email: authorEmail,
|
||||
time: +authorTime * 1000,
|
||||
},
|
||||
committer: {
|
||||
name: committerName,
|
||||
email: committerEmail,
|
||||
time: +committerTime * 1000,
|
||||
},
|
||||
branch: branch.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
const diffLimit = 1_000_000; // 1MB
|
||||
if (envInfo['pull.base']) {
|
||||
const pullDiffResult = await spawnAsync(
|
||||
'git',
|
||||
['diff', envInfo['pull.base']],
|
||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||
);
|
||||
if (!pullDiffResult.code)
|
||||
result['pull.diff'] = pullDiffResult.stdout.substring(0, diffLimit);
|
||||
} else {
|
||||
const diffResult = await spawnAsync(
|
||||
'git',
|
||||
['diff', 'HEAD~1'],
|
||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||
);
|
||||
if (!diffResult.code)
|
||||
result['revision.diff'] = diffResult.stdout.substring(0, diffLimit);
|
||||
async function gitDiff(gitDir: string, ci?: CIInfo): Promise<string | undefined> {
|
||||
const diffLimit = 100_000;
|
||||
if (ci) {
|
||||
// First try the diff against the base branch.
|
||||
const diff = await runGit(`git diff ${ci.baseHash}`, gitDir);
|
||||
if (diff)
|
||||
return diff.substring(0, diffLimit);
|
||||
|
||||
// Grow history for shallow checkout.
|
||||
const output = await runGit('git fetch --deepen=1 && git show HEAD', gitDir);
|
||||
return output?.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 { gitCommitInfo } from './gitCommitInfoPlugin';
|
||||
|
|
|
|||
|
|
@ -270,7 +270,8 @@ export class TerminalReporter implements ReporterV2 {
|
|||
if (full && summary.failuresToPrint.length && !this._omitFailures)
|
||||
this._printFailures(summary.failuresToPrint);
|
||||
this._printSlowTests();
|
||||
this._printWarnings();
|
||||
// TODO: 1.52: Make warning display prettier
|
||||
// this._printWarnings();
|
||||
this._printSummary(summaryMessage);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -206,8 +206,8 @@ export function addSuffixToFilePath(filePath: string, suffix: string): string {
|
|||
return base + suffix + ext;
|
||||
}
|
||||
|
||||
export function sanitizeFilePathBeforeExtension(filePath: string): string {
|
||||
const ext = path.extname(filePath);
|
||||
export function sanitizeFilePathBeforeExtension(filePath: string, ext?: string): string {
|
||||
ext ??= path.extname(filePath);
|
||||
const base = filePath.substring(0, filePath.length - ext.length);
|
||||
return sanitizeForFilePath(base) + ext;
|
||||
}
|
||||
|
|
@ -391,6 +391,15 @@ function fileExists(resolved: string) {
|
|||
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) {
|
||||
return fs.statSync(resolved, { throwIfNoEntry: false })?.isDirectory();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class Fixture {
|
|||
private _selfTeardownComplete: Promise<void> | undefined;
|
||||
private _setupDescription: FixtureDescription;
|
||||
private _teardownDescription: FixtureDescription;
|
||||
private _stepInfo: { category: 'fixture', location?: Location } | undefined;
|
||||
private _stepInfo: { title: string, category: 'fixture', location?: Location } | undefined;
|
||||
_deps = new Set<Fixture>();
|
||||
_usages = new Set<Fixture>();
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ class Fixture {
|
|||
const isUserFixture = this.registration.location && filterStackFile(this.registration.location.file);
|
||||
const title = this.registration.customTitle || this.registration.name;
|
||||
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 = {
|
||||
title,
|
||||
phase: 'setup',
|
||||
|
|
@ -68,13 +68,11 @@ class Fixture {
|
|||
return;
|
||||
}
|
||||
|
||||
await testInfo._runAsStage({
|
||||
title: `fixture: ${this.registration.customTitle ?? this.registration.name}`,
|
||||
runnable: { ...runnable, fixture: this._setupDescription },
|
||||
stepInfo: this._stepInfo,
|
||||
}, async () => {
|
||||
await this._setupInternal(testInfo);
|
||||
});
|
||||
const run = () => testInfo._runWithTimeout({ ...runnable, fixture: this._setupDescription }, () => this._setupInternal(testInfo));
|
||||
if (this._stepInfo)
|
||||
await testInfo._runAsStep(this._stepInfo, run);
|
||||
else
|
||||
await run();
|
||||
}
|
||||
|
||||
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
|
||||
// time remaining in the time slot. This avoids cascading timeouts.
|
||||
if (!testInfo._timeoutManager.isTimeExhaustedFor(fixtureRunnable)) {
|
||||
await testInfo._runAsStage({
|
||||
title: `fixture: ${this.registration.customTitle ?? this.registration.name}`,
|
||||
runnable: fixtureRunnable,
|
||||
stepInfo: this._stepInfo,
|
||||
}, async () => {
|
||||
await this._teardownInternal();
|
||||
});
|
||||
const run = () => testInfo._runWithTimeout(fixtureRunnable, () => this._teardownInternal());
|
||||
if (this._stepInfo)
|
||||
await testInfo._runAsStep(this._stepInfo, run);
|
||||
else
|
||||
await run();
|
||||
}
|
||||
} finally {
|
||||
// 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.
|
||||
return null;
|
||||
}
|
||||
await testInfo._runAsStage({ title: 'run function', runnable }, async () => {
|
||||
await fn(params, testInfo);
|
||||
});
|
||||
await testInfo._runWithTimeout(runnable, () => fn(params, testInfo));
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
wrapPromiseAPIResult<T>(promise: Promise<T>): Promise<T> {
|
||||
if (process.env.PW_DISABLE_FLOATING_PROMISES_WARNING)
|
||||
return promise;
|
||||
|
||||
const promiseProxy = new Proxy(promise, {
|
||||
get: (target, prop, receiver) => {
|
||||
if (prop === 'then') {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import path from 'path';
|
|||
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils';
|
||||
|
||||
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 { testInfoError } from './util';
|
||||
import { FloatingPromiseScope } from './floatingPromiseScope';
|
||||
|
|
@ -50,16 +50,8 @@ export interface TestStepInternal {
|
|||
error?: TestInfoErrorImpl;
|
||||
infectParentStepsWithError?: 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 {
|
||||
private _onStepBegin: (payload: StepBeginPayload) => void;
|
||||
private _onStepEnd: (payload: StepEndPayload) => void;
|
||||
|
|
@ -235,28 +227,27 @@ export class TestInfoImpl implements TestInfo {
|
|||
}
|
||||
}
|
||||
|
||||
private _findLastStageStep(steps: TestStepInternal[]): TestStepInternal | undefined {
|
||||
// Find the deepest step that is marked as isStage and has not finished yet.
|
||||
private _findLastPredefinedStep(steps: TestStepInternal[]): TestStepInternal | undefined {
|
||||
// Find the deepest predefined step that has not finished yet.
|
||||
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)
|
||||
return child;
|
||||
if (steps[i].isStage && !steps[i].endWallTime)
|
||||
if ((steps[i].category === 'hook' || steps[i].category === 'fixture') && !steps[i].endWallTime)
|
||||
return steps[i];
|
||||
}
|
||||
}
|
||||
|
||||
private _parentStep() {
|
||||
return currentZone().data<TestStepInternal>('stepZone')
|
||||
?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
|
||||
return currentZone().data<TestStepInternal>('stepZone') ?? this._findLastPredefinedStep(this._steps);
|
||||
}
|
||||
|
||||
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices' | 'info'>, parentStep?: TestStepInternal): TestStepInternal {
|
||||
const stepId = `${data.category}@${++this._lastStepId}`;
|
||||
|
||||
if (data.isStage) {
|
||||
// Predefined stages form a fixed hierarchy - use the current one as parent.
|
||||
parentStep = this._findLastStageStep(this._steps);
|
||||
if (data.category === 'hook' || data.category === 'fixture') {
|
||||
// Predefined steps form a fixed hierarchy - use the current one as parent.
|
||||
parentStep = this._findLastPredefinedStep(this._steps);
|
||||
} else {
|
||||
if (!parentStep)
|
||||
parentStep = this._parentStep();
|
||||
|
|
@ -355,21 +346,23 @@ export class TestInfoImpl implements TestInfo {
|
|||
this._tracing.appendForError(serialized);
|
||||
}
|
||||
|
||||
async _runAsStage(stage: TestStage, cb: () => Promise<any>) {
|
||||
if (debugTest.enabled) {
|
||||
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;
|
||||
|
||||
async _runAsStep(stepInfo: { title: string, category: 'hook' | 'fixture', location?: Location }, cb: () => Promise<any>) {
|
||||
const step = this._addStep(stepInfo);
|
||||
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 {
|
||||
await cb();
|
||||
} catch (e) {
|
||||
// Only handle errors directly thrown by the user code.
|
||||
if (!stage.runnable)
|
||||
throw e;
|
||||
if (this._allowSkips && (e instanceof SkipError)) {
|
||||
if (this.status === 'passed')
|
||||
this.status = 'skipped';
|
||||
|
|
@ -377,7 +370,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
// Unfortunately, we have to handle user errors and timeout errors differently.
|
||||
// Consider the following scenario:
|
||||
// - 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
|
||||
// - 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
|
||||
|
|
@ -386,16 +379,12 @@ export class TestInfoImpl implements TestInfo {
|
|||
throw e;
|
||||
}
|
||||
});
|
||||
stage.step?.complete({});
|
||||
} catch (error) {
|
||||
// When interrupting, we arrive here with a TimeoutManagerError, but we should not
|
||||
// consider it a timeout.
|
||||
if (!this._wasInterrupted && (error instanceof TimeoutManagerError) && stage.runnable)
|
||||
if (!this._wasInterrupted && (error instanceof TimeoutManagerError))
|
||||
this._failWithError(error);
|
||||
stage.step?.complete({ error });
|
||||
throw error;
|
||||
} finally {
|
||||
debugTest(`finished stage "${stage.title}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -430,7 +419,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
} else {
|
||||
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
|
||||
const callId = `attach@${++this._lastStepId}`;
|
||||
this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, 'attach', `attach "${attachment.name}"`, undefined, []);
|
||||
this._tracing.appendBeforeActionForStep(callId, this._findLastPredefinedStep(this._steps)?.stepId, 'attach', `attach "${attachment.name}"`, undefined, []);
|
||||
this._tracing.appendAfterActionForStep(callId, undefined, [attachment]);
|
||||
}
|
||||
|
||||
|
|
@ -463,9 +452,11 @@ export class TestInfoImpl implements TestInfo {
|
|||
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 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 parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
||||
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
|
||||
|
|
@ -481,8 +472,8 @@ export class TestInfoImpl implements TestInfo {
|
|||
.replace(/\{(.)?testName\}/g, '$1' + this._fsSanitizedTestName())
|
||||
.replace(/\{(.)?testFileName\}/g, '$1' + parsedRelativeTestFilePath.base)
|
||||
.replace(/\{(.)?testFilePath\}/g, '$1' + relativeTestFilePath)
|
||||
.replace(/\{(.)?arg\}/g, '$1' + path.join(parsedSubPath.dir, parsedSubPath.name))
|
||||
.replace(/\{(.)?ext\}/g, parsedSubPath.ext ? '$1' + parsedSubPath.ext : '');
|
||||
.replace(/\{(.)?arg\}/g, '$1' + path.join(dir, name))
|
||||
.replace(/\{(.)?ext\}/g, ext ? '$1' + ext : '');
|
||||
|
||||
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
import { ManualPromise, monotonicTime } from 'playwright-core/lib/utils';
|
||||
import { colors } from 'playwright-core/lib/utils';
|
||||
|
||||
import { debugTest, formatLocation } from '../util';
|
||||
|
||||
import type { Location } from '../../types/testReporter';
|
||||
|
||||
export type TimeSlot = {
|
||||
|
|
@ -76,9 +78,7 @@ export class TimeoutManager {
|
|||
return slot.timeout > 0 && (slot.elapsed >= slot.timeout - 1);
|
||||
}
|
||||
|
||||
async withRunnable<T>(runnable: RunnableDescription | undefined, cb: () => Promise<T>): Promise<T> {
|
||||
if (!runnable)
|
||||
return await cb();
|
||||
async withRunnable<T>(runnable: RunnableDescription, cb: () => Promise<T>): Promise<T> {
|
||||
if (this._running)
|
||||
throw new Error(`Internal error: duplicate runnable`);
|
||||
const running = this._running = {
|
||||
|
|
@ -89,7 +89,13 @@ export class TimeoutManager {
|
|||
timer: undefined,
|
||||
timeoutPromise: new ManualPromise(),
|
||||
};
|
||||
let debugTitle = '';
|
||||
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);
|
||||
return await Promise.race([
|
||||
cb(),
|
||||
|
|
@ -101,6 +107,8 @@ export class TimeoutManager {
|
|||
running.timer = undefined;
|
||||
running.slot.elapsed += monotonicTime() - running.start;
|
||||
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 runnable = { type: 'teardown' } as const;
|
||||
// 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('worker', fakeTestInfo, runnable).catch(() => {});
|
||||
// Close any other browsers launched in this process. This includes anything launched
|
||||
// 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);
|
||||
} catch (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.
|
||||
const checkForFloatingPromises = (functionDescription: string) => {
|
||||
if (process.env.PW_DISABLE_FLOATING_PROMISES_WARNING)
|
||||
return;
|
||||
if (!testInfo._floatingPromiseScope.hasFloatingPromises())
|
||||
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();
|
||||
};
|
||||
|
||||
await testInfo._runAsStage({ title: 'setup and test' }, async () => {
|
||||
await testInfo._runAsStage({ title: 'start tracing', runnable: { type: 'test' } }, async () => {
|
||||
await (async () => {
|
||||
await testInfo._runWithTimeout({ type: 'test' }, async () => {
|
||||
// Ideally, "trace" would be an config-level option belonging to the
|
||||
// test runner instead of a fixture belonging to Playwright.
|
||||
// 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]);
|
||||
|
||||
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.
|
||||
for (const suite of suites)
|
||||
await this._runBeforeAllHooksForSuite(suite, testInfo);
|
||||
|
|
@ -376,13 +379,13 @@ export class WorkerMain extends ProcessRunner {
|
|||
return;
|
||||
}
|
||||
|
||||
await testInfo._runAsStage({ title: 'test function', runnable: { type: 'test' } }, async () => {
|
||||
await testInfo._runWithTimeout({ type: 'test' }, async () => {
|
||||
// Now run the test itself.
|
||||
const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
|
||||
await fn(testFunctionParams, testInfo);
|
||||
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.
|
||||
testInfo.duration = testInfo._timeoutManager.defaultSlot().elapsed | 0;
|
||||
|
|
@ -393,12 +396,12 @@ export class WorkerMain extends ProcessRunner {
|
|||
// After hooks get an additional timeout.
|
||||
const afterHooksTimeout = calculateMaxTimeout(this._project.project.timeout, testInfo.timeout);
|
||||
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;
|
||||
|
||||
try {
|
||||
// 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) {
|
||||
firstAfterHooksError = firstAfterHooksError ?? error;
|
||||
}
|
||||
|
|
@ -448,7 +451,7 @@ export class WorkerMain extends ProcessRunner {
|
|||
// Mark as "cleaned up" early to avoid running cleanup twice.
|
||||
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;
|
||||
|
||||
// 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 };
|
||||
await testInfo._runAsStage({ title: 'stop tracing', runnable: { type: 'test', slot: tracingSlot } }, async () => {
|
||||
await testInfo._runWithTimeout({ type: 'test', slot: tracingSlot }, async () => {
|
||||
await testInfo._tracing.stopIfNeeded();
|
||||
}).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;
|
||||
for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) {
|
||||
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.
|
||||
const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
||||
const runnable = { type: hook.type, slot: timeSlot, location: hook.location };
|
||||
|
|
@ -587,7 +590,7 @@ export class WorkerMain extends ProcessRunner {
|
|||
continue;
|
||||
}
|
||||
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);
|
||||
});
|
||||
} 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({
|
||||
* webServer: {
|
||||
* command: 'npm run start',
|
||||
* url: 'http://127.0.0.1:3000',
|
||||
* url: 'http://localhost:3000',
|
||||
* timeout: 120 * 1000,
|
||||
* reuseExistingServer: !process.env.CI,
|
||||
* },
|
||||
|
|
@ -915,19 +915,19 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
* webServer: [
|
||||
* {
|
||||
* command: 'npm run start',
|
||||
* url: 'http://127.0.0.1:3000',
|
||||
* url: 'http://localhost:3000',
|
||||
* timeout: 120 * 1000,
|
||||
* reuseExistingServer: !process.env.CI,
|
||||
* },
|
||||
* {
|
||||
* command: 'npm run backend',
|
||||
* url: 'http://127.0.0.1:3333',
|
||||
* url: 'http://localhost:3333',
|
||||
* timeout: 120 * 1000,
|
||||
* reuseExistingServer: !process.env.CI,
|
||||
* }
|
||||
* ],
|
||||
* 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
|
||||
* 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
|
||||
* [testConfig.populateGitInfo](https://playwright.dev/docs/api/class-testconfig#test-config-populate-git-info) that
|
||||
* populates metadata.
|
||||
* On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic
|
||||
* generation.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
|
|
@ -1360,29 +1361,6 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
*/
|
||||
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
|
||||
* [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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
|
@ -1917,12 +1895,12 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Test duration in milliseconds that is considered slow. Defaults to 15 seconds.
|
||||
* Test file duration in milliseconds that is considered slow.
|
||||
*/
|
||||
threshold: number;
|
||||
};
|
||||
|
|
@ -8815,14 +8793,14 @@ interface LocatorAssertions {
|
|||
/**
|
||||
* 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`
|
||||
* 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**
|
||||
*
|
||||
* ```js
|
||||
* 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
|
||||
|
|
@ -8924,13 +8902,25 @@ interface PageAssertions {
|
|||
* **Usage**
|
||||
*
|
||||
* ```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
|
||||
* [`baseURL`](https://playwright.dev/docs/api/class-browser#browser-new-context-option-base-url) via the context
|
||||
* options was provided and the passed URL is a path, it gets merged via the
|
||||
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||
* @param 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) 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.
|
||||
* @param 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,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
failOnStatusCode?: boolean,
|
||||
clientCertificates?: {
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
|
|
@ -655,7 +655,7 @@ export type PlaywrightNewRequestOptions = {
|
|||
userAgent?: string,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
failOnStatusCode?: boolean,
|
||||
clientCertificates?: {
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
|
|
@ -1029,7 +1029,6 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1111,7 +1110,6 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1228,7 +1226,6 @@ export type BrowserNewContextParams = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1296,7 +1293,6 @@ export type BrowserNewContextOptions = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1367,7 +1363,6 @@ export type BrowserNewContextForReuseParams = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1435,7 +1430,6 @@ export type BrowserNewContextForReuseOptions = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -4802,7 +4796,6 @@ export type AndroidDeviceLaunchBrowserParams = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -4868,7 +4861,6 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
|
|||
|
|
@ -520,7 +520,6 @@ ContextOptions:
|
|||
extraHTTPHeaders:
|
||||
type: array?
|
||||
items: NameValue
|
||||
apiRequestFailsOnErrorStatus: boolean?
|
||||
offline: boolean?
|
||||
httpCredentials:
|
||||
type: object?
|
||||
|
|
@ -752,7 +751,7 @@ Playwright:
|
|||
extraHTTPHeaders:
|
||||
type: array?
|
||||
items: NameValue
|
||||
apiRequestFailsOnErrorStatus: boolean?
|
||||
failOnStatusCode: boolean?
|
||||
clientCertificates:
|
||||
type: array?
|
||||
items:
|
||||
|
|
|
|||
|
|
@ -24,20 +24,21 @@ import type { StackFrame } from '@protocol/channels';
|
|||
import { CopyToClipboardTextButton } from './copyToClipboard';
|
||||
import { attachmentURL } from './attachmentsTab';
|
||||
import { fixTestPrompt } from '@web/components/prompts';
|
||||
import type { GitCommitInfo } from '@testIsomorphic/types';
|
||||
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||
import { AIConversation } from './aiConversation';
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
import { useIsLLMAvailable, useLLMChat } from './llm';
|
||||
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 }>) {
|
||||
return <GitCommitInfoContext.Provider value={gitCommitInfo}>{children}</GitCommitInfoContext.Provider>;
|
||||
export function CommitInfoProvider({ children, commitInfo }: React.PropsWithChildren<{ commitInfo: MetadataWithCommitInfo }>) {
|
||||
return <CommitInfoContext.Provider value={commitInfo}>{children}</CommitInfoContext.Provider>;
|
||||
}
|
||||
|
||||
export function useGitCommitInfo() {
|
||||
return React.useContext(GitCommitInfoContext);
|
||||
export function useCommitInfo() {
|
||||
return React.useContext(CommitInfoContext);
|
||||
}
|
||||
|
||||
function usePageSnapshot(actions: modelUtil.ActionTraceEventInContext[]) {
|
||||
|
|
@ -53,18 +54,47 @@ function usePageSnapshot(actions: modelUtil.ActionTraceEventInContext[]) {
|
|||
}, [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<{
|
||||
error: string;
|
||||
codeFrame: string;
|
||||
pageSnapshot?: string;
|
||||
diff?: string;
|
||||
}> = ({ error, pageSnapshot, diff }) => {
|
||||
}> = ({ error, codeFrame, pageSnapshot, diff }) => {
|
||||
const prompt = React.useMemo(
|
||||
() => fixTestPrompt(
|
||||
error,
|
||||
error + '\n\n' + codeFrame,
|
||||
diff,
|
||||
pageSnapshot
|
||||
),
|
||||
[error, diff, pageSnapshot]
|
||||
[error, diff, codeFrame, pageSnapshot]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -97,11 +127,10 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined):
|
|||
}, [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 llmAvailable = useIsLLMAvailable();
|
||||
const gitCommitInfo = useGitCommitInfo();
|
||||
const diff = gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'];
|
||||
const metadata = useCommitInfo();
|
||||
|
||||
let location: string | undefined;
|
||||
let longLocation: string | undefined;
|
||||
|
|
@ -112,6 +141,8 @@ function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSou
|
|||
longLocation = stackFrame.file + ':' + stackFrame.line;
|
||||
}
|
||||
|
||||
const codeFrame = useCodeFrame(error.stack, sources, 3);
|
||||
|
||||
return <div style={{ display: 'flex', flexDirection: 'column', overflowX: 'clip' }}>
|
||||
<div className='hbox' style={{
|
||||
alignItems: 'center',
|
||||
|
|
@ -127,8 +158,8 @@ function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSou
|
|||
</div>}
|
||||
<span style={{ position: 'absolute', right: '5px' }}>
|
||||
{llmAvailable
|
||||
? <FixWithAIButton conversationId={errorId} onChange={setShowLLM} value={showLLM} error={message} diff={diff} pageSnapshot={pageSnapshot} />
|
||||
: <CopyPromptButton error={message} pageSnapshot={pageSnapshot} diff={diff} />}
|
||||
? <FixWithAIButton conversationId={errorId} onChange={setShowLLM} value={showLLM} error={message} diff={metadata?.gitDiff} pageSnapshot={pageSnapshot} />
|
||||
: <CopyPromptButton error={message} codeFrame={codeFrame} pageSnapshot={pageSnapshot} diff={metadata?.gitDiff} />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -180,9 +211,10 @@ export const ErrorsTab: React.FunctionComponent<{
|
|||
errorsModel: ErrorsTabModel,
|
||||
actions: modelUtil.ActionTraceEventInContext[],
|
||||
wallTime: number,
|
||||
sources: Map<string, modelUtil.SourceModel>,
|
||||
sdkLanguage: Language,
|
||||
revealInSource: (error: ErrorDescription) => void,
|
||||
}> = ({ errorsModel, sdkLanguage, revealInSource, actions, wallTime }) => {
|
||||
}> = ({ errorsModel, sdkLanguage, revealInSource, actions, wallTime, sources }) => {
|
||||
const pageSnapshot = usePageSnapshot(actions);
|
||||
|
||||
if (!errorsModel.errors.size)
|
||||
|
|
@ -191,7 +223,7 @@ export const ErrorsTab: React.FunctionComponent<{
|
|||
return <div className='fill' style={{ overflow: 'auto' }}>
|
||||
{[...errorsModel.errors.entries()].map(([message, error]) => {
|
||||
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>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,25 +27,8 @@ import { CopyToClipboard } from './copyToClipboard';
|
|||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
import { Toolbar } from '@web/components/toolbar';
|
||||
|
||||
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 } = useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[], location?: SourceLocation }>(async () => {
|
||||
export function useSources(stack: StackFrame[] | undefined, selectedFrame: number, sources: Map<string, SourceModel>, rootDir?: string, fallbackLocation?: SourceLocation) {
|
||||
return useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[], location?: SourceLocation }>(async () => {
|
||||
const actionLocation = stack?.[selectedFrame];
|
||||
const shouldUseFallback = !actionLocation?.file;
|
||||
if (shouldUseFallback && !fallbackLocation)
|
||||
|
|
@ -84,6 +67,27 @@ export const SourceTab: React.FunctionComponent<{
|
|||
}
|
||||
return { source, highlight, targetLine, fileName, location };
|
||||
}, [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(() => {
|
||||
if (!location)
|
||||
|
|
|
|||
|
|
@ -37,8 +37,9 @@ import { TestListView } from './uiModeTestListView';
|
|||
import { TraceView } from './uiModeTraceView';
|
||||
import { SettingsView } from './settingsView';
|
||||
import { DefaultSettingsView } from './defaultSettingsView';
|
||||
import { GitCommitInfoProvider } from './errorsTab';
|
||||
import { CommitInfoProvider } from './errorsTab';
|
||||
import { LLMProvider } from './llm';
|
||||
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||
|
||||
let xtermSize = { cols: 80, rows: 24 };
|
||||
const xtermDataSource: XtermDataSource = {
|
||||
|
|
@ -432,7 +433,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
||||
</div>
|
||||
<div className={clsx('vbox', isShowingOutput && 'hidden')}>
|
||||
<GitCommitInfoProvider gitCommitInfo={testModel?.config.metadata['git.commit.info']}>
|
||||
<CommitInfoProvider commitInfo={testModel?.config.metadata as MetadataWithCommitInfo}>
|
||||
<TraceView
|
||||
pathSeparator={queryParams.pathSeparator}
|
||||
item={selectedItem}
|
||||
|
|
@ -440,7 +441,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||
revealSource={revealSource}
|
||||
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
|
||||
/>
|
||||
</GitCommitInfoProvider>
|
||||
</CommitInfoProvider>
|
||||
</div>
|
||||
</div>}
|
||||
sidebar={<div className='vbox ui-mode-sidebar'>
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
id: 'errors',
|
||||
title: 'Errors',
|
||||
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)
|
||||
setSelectedAction(error.action);
|
||||
else
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ import type { ReporterDescription } from '@playwright/test';
|
|||
|
||||
const reporters = () => {
|
||||
const result: ReporterDescription[] = process.env.CI ? [
|
||||
['html'],
|
||||
['blob'],
|
||||
['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }],
|
||||
] : [
|
||||
['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 state = await context.storageState({ path });
|
||||
const state = await context.storageState({ path, indexedDB: true });
|
||||
const written = await fs.promises.readFile(path, 'utf8');
|
||||
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.getByText('Add Task').click();
|
||||
|
||||
const storageState = await page.context().storageState();
|
||||
const storageState = await page.context().storageState({ indexedDB: true });
|
||||
expect(storageState.origins).toEqual([
|
||||
{
|
||||
origin: server.PREFIX,
|
||||
|
|
@ -438,7 +438,7 @@ it('should support IndexedDB', async ({ page, server, contextFactory }) => {
|
|||
]);
|
||||
|
||||
const context = await contextFactory({ storageState });
|
||||
expect(await context.storageState()).toEqual(storageState);
|
||||
expect(await context.storageState({ indexedDB: true })).toEqual(storageState);
|
||||
|
||||
const recreatedPage = await context.newPage();
|
||||
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/
|
||||
`);
|
||||
|
||||
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 });
|
||||
await request.get(server.EMPTY_PAGE);
|
||||
const exportedState = await request.storageState();
|
||||
const exportedState = await request.storageState({ indexedDB: true });
|
||||
expect(exportedState).toEqual(storageState);
|
||||
await request.dispose();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -537,9 +537,9 @@ it('should retry ECONNRESET', {
|
|||
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' });
|
||||
const request = await playwright.request.newContext({ apiRequestFailsOnErrorStatus: true });
|
||||
const request = await playwright.request.newContext({ failOnStatusCode: true });
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
||||
res.end('Not found.');
|
||||
|
|
@ -549,9 +549,9 @@ it('should throw when apiRequestFailsOnErrorStatus is set to true inside APIRequ
|
|||
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' });
|
||||
const request = await playwright.request.newContext({ apiRequestFailsOnErrorStatus: false });
|
||||
const request = await playwright.request.newContext({ failOnStatusCode: false });
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
||||
res.end('Not found.');
|
||||
|
|
|
|||
|
|
@ -92,13 +92,14 @@ it('should support locator.or()', 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.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').first().element.innerHTML`)).toContain('Hello');
|
||||
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('div').filter({ visible: false }).element.innerHTML`)).toContain('two');
|
||||
});
|
||||
|
||||
it('expected properties on playwright object', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -321,23 +321,17 @@ it('reverse engineer hasNotText', async ({ page }) => {
|
|||
});
|
||||
|
||||
it('reverse engineer visible', async ({ page }) => {
|
||||
expect.soft(generate(page.getByText('Hello').visible().locator('div'))).toEqual({
|
||||
csharp: `GetByText("Hello").Visible().Locator("div")`,
|
||||
java: `getByText("Hello").visible().locator("div")`,
|
||||
javascript: `getByText('Hello').visible().locator('div')`,
|
||||
python: `get_by_text("Hello").visible().locator("div")`,
|
||||
expect.soft(generate(page.getByText('Hello').filter({ visible: true }).locator('div'))).toEqual({
|
||||
csharp: `GetByText("Hello").Filter(new() { Visible = true }).Locator("div")`,
|
||||
java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(true)).locator("div")`,
|
||||
javascript: `getByText('Hello').filter({ visible: true }).locator('div')`,
|
||||
python: `get_by_text("Hello").filter(visible=True).locator("div")`,
|
||||
});
|
||||
expect.soft(generate(page.getByText('Hello').visible({ visible: true }).locator('div'))).toEqual({
|
||||
csharp: `GetByText("Hello").Visible().Locator("div")`,
|
||||
java: `getByText("Hello").visible().locator("div")`,
|
||||
javascript: `getByText('Hello').visible().locator('div')`,
|
||||
python: `get_by_text("Hello").visible().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")`,
|
||||
expect.soft(generate(page.getByText('Hello').filter({ visible: false }).locator('div'))).toEqual({
|
||||
csharp: `GetByText("Hello").Filter(new() { Visible = false }).Locator("div")`,
|
||||
java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(false)).locator("div")`,
|
||||
javascript: `getByText('Hello').filter({ visible: false }).locator('div')`,
|
||||
python: `get_by_text("Hello").filter(visible=False).locator("div")`,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -372,6 +372,13 @@ test('display:contents should be visible when contents are visible', async ({ pa
|
|||
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.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 });
|
||||
// Should support falsy ignoreCase.
|
||||
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 }) => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
it('should support .visible()', async ({ page }) => {
|
||||
it('should support filter(visible)', async ({ page }) => {
|
||||
await page.setContent(`<div>
|
||||
<div class="item" style="display: none">Hidden data0</div>
|
||||
<div class="item">visible data1</div>
|
||||
|
|
@ -160,11 +160,10 @@ it('should support .visible()', async ({ page }) => {
|
|||
<div class="item">visible data3</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(page.locator('.item').visible().getByText('data3')).toHaveText('visible data3');
|
||||
await expect(page.locator('.item').visible({ visible: true }).getByText('data2')).toHaveText('visible data2');
|
||||
await expect(page.locator('.item').visible({ visible: false }).getByText('data1')).toHaveText('Hidden data1');
|
||||
await expect(page.locator('.item').filter({ visible: true }).getByText('data3')).toHaveText('visible data3');
|
||||
await expect(page.locator('.item').filter({ visible: false }).getByText('data1')).toHaveText('Hidden data1');
|
||||
});
|
||||
|
||||
it('locator.count should work with deleted Map in main world', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -515,6 +515,7 @@ it('should normalize whitespace', async ({ page }) => {
|
|||
<summary> one \n two <a href="#"> link \n 1 </a> </summary>
|
||||
</details>
|
||||
<input value=' hello world '>
|
||||
<button>hello\u00ad\u200bworld</button>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
|
|
@ -522,6 +523,7 @@ it('should normalize whitespace', async ({ page }) => {
|
|||
- text: one two
|
||||
- link "link 1"
|
||||
- textbox: hello world
|
||||
- button "helloworld"
|
||||
`);
|
||||
|
||||
// Weird whitespace in the template should be normalized.
|
||||
|
|
@ -532,6 +534,7 @@ it('should normalize whitespace', async ({ page }) => {
|
|||
two
|
||||
- link " link 1 "
|
||||
- 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.check('input');
|
||||
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 }) => {
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ test.describe.configure({ mode: 'parallel' });
|
|||
|
||||
test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts-snapshots/test.yml': `
|
||||
'a.spec.ts-snapshots/test.snapshot.yml': `
|
||||
- heading "hello world"
|
||||
`,
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
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';
|
||||
test('test', async ({ page }) => {
|
||||
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 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.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-2.yml, writing actual`);
|
||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
|
||||
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.snapshot.yml, writing actual`);
|
||||
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]');
|
||||
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]');
|
||||
});
|
||||
|
||||
test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts-snapshots/test-1.yml': `
|
||||
'a.spec.ts-snapshots/test-1.snapshot.yml': `
|
||||
- heading "foo"
|
||||
`,
|
||||
'a.spec.ts-snapshots/test-2.yml': `
|
||||
'a.spec.ts-snapshots/test-2.snapshot.yml': `
|
||||
- heading "bar"
|
||||
`,
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
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 expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-2.yml' });
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-2.snapshot.yml' });
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': 'all' });
|
||||
|
||||
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-2.yml`);
|
||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
|
||||
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.snapshot.yml`);
|
||||
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]');
|
||||
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]');
|
||||
});
|
||||
|
||||
test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts-snapshots/test.yml': `
|
||||
'a.spec.ts-snapshots/test.snapshot.yml': `
|
||||
- heading "hello world"
|
||||
`,
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
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' });
|
||||
|
||||
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"');
|
||||
});
|
||||
|
||||
|
|
@ -120,14 +120,32 @@ test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
|
|||
});
|
||||
|
||||
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-2.yml, writing actual`);
|
||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-1.yml'), 'utf8');
|
||||
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.snapshot.yml, writing actual`);
|
||||
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]');
|
||||
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]');
|
||||
});
|
||||
|
||||
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']) {
|
||||
test(`should update snapshot with the update-snapshots=${updateSnapshots} (config)`, async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
|
|
@ -143,13 +161,13 @@ for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
|
|||
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';
|
||||
expect(result.exitCode).toBe(rebase ? 0 : 1);
|
||||
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`);
|
||||
const data = fs.readFileSync(snapshotOutputPath);
|
||||
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 });
|
||||
});
|
||||
`,
|
||||
'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);
|
||||
|
|
@ -183,14 +201,14 @@ test('should respect config.snapshotPathTemplate', async ({ runInlineTest }, tes
|
|||
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"
|
||||
`,
|
||||
'dir/a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
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"
|
||||
`,
|
||||
'actual-snapshots/dir/a.spec.ts/test.yml': `
|
||||
'actual-snapshots/dir/a.spec.ts/test.snapshot.yml': `
|
||||
- heading "hello world"
|
||||
`,
|
||||
'dir/a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
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([
|
||||
'.last-run.json',
|
||||
'artifacts-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
'artifacts-own-context-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
'artifacts-own-context-passing',
|
||||
' test-finished-1.ariasnapshot',
|
||||
' test-finished-1.snapshot.yml',
|
||||
'artifacts-passing',
|
||||
' test-finished-1.ariasnapshot',
|
||||
' test-finished-1.snapshot.yml',
|
||||
'artifacts-persistent-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
'artifacts-persistent-passing',
|
||||
' test-finished-1.ariasnapshot',
|
||||
' test-finished-1.snapshot.yml',
|
||||
'artifacts-shared-shared-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-2.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
' test-failed-2.snapshot.yml',
|
||||
'artifacts-shared-shared-passing',
|
||||
' test-finished-1.ariasnapshot',
|
||||
' test-finished-2.ariasnapshot',
|
||||
' test-finished-1.snapshot.yml',
|
||||
' test-finished-2.snapshot.yml',
|
||||
'artifacts-two-contexts',
|
||||
' test-finished-1.ariasnapshot',
|
||||
' test-finished-2.ariasnapshot',
|
||||
' test-finished-1.snapshot.yml',
|
||||
' test-finished-2.snapshot.yml',
|
||||
'artifacts-two-contexts-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-2.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
' 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([
|
||||
'.last-run.json',
|
||||
'artifacts-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
'artifacts-own-context-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
'artifacts-persistent-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
'artifacts-shared-shared-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-2.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
' test-failed-2.snapshot.yml',
|
||||
'artifacts-two-contexts-failing',
|
||||
' test-failed-1.ariasnapshot',
|
||||
' test-failed-2.ariasnapshot',
|
||||
' test-failed-1.snapshot.yml',
|
||||
' 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 = {
|
||||
'uncommitted.txt': `uncommitted file`,
|
||||
'playwright.config.ts': `
|
||||
export default {
|
||||
populateGitInfo: true,
|
||||
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' }, {
|
||||
PLAYWRIGHT_HTML_OPEN: 'never',
|
||||
GITHUB_ACTIONS: '1',
|
||||
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
||||
GITHUB_SHA: 'example-sha',
|
||||
|
|
@ -1230,19 +1230,22 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
expect(result.exitCode).toBe(0);
|
||||
await page.getByRole('button', { name: 'Metadata' }).click();
|
||||
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
||||
- 'link "chore(html): make this test look nice"'
|
||||
- text: /^William <shakespeare@example.local> on/
|
||||
- link /^[a-f0-9]{7}$/
|
||||
- text: 'foo : value1 bar : {"prop":"value2"} baz : ["value3",123]'
|
||||
- list:
|
||||
- listitem:
|
||||
- 'link "chore(html): make this test look nice"'
|
||||
- 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 = {
|
||||
'uncommitted.txt': `uncommitted file`,
|
||||
'playwright.config.ts': `
|
||||
export default {
|
||||
populateGitInfo: true,
|
||||
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' }, {
|
||||
PLAYWRIGHT_HTML_OPEN: 'never',
|
||||
GITHUB_ACTIONS: '1',
|
||||
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
||||
GITHUB_RUN_ID: 'example-run-id',
|
||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
||||
|
|
@ -1291,18 +1295,21 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
expect(result.exitCode).toBe(0);
|
||||
await page.getByRole('button', { name: 'Metadata' }).click();
|
||||
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
||||
- 'link "My PR"'
|
||||
- text: /^William <shakespeare@example.local> on/
|
||||
- link "Logs"
|
||||
- link "Pull Request"
|
||||
- text: 'foo : value1 bar : {"prop":"value2"} baz : ["value3",123]'
|
||||
- list:
|
||||
- listitem:
|
||||
- link "My PR"
|
||||
- listitem: /William <shakespeare@example.local>/
|
||||
- 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({
|
||||
'playwright.config.ts': `
|
||||
export default { populateGitInfo: false };
|
||||
export default {};
|
||||
`,
|
||||
'example.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
|
@ -1323,7 +1330,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
'playwright.config.ts': `
|
||||
export default {
|
||||
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`,
|
||||
'playwright.config.ts': `
|
||||
export default {
|
||||
populateGitInfo: true,
|
||||
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
||||
metadata: {
|
||||
foo: 'value1',
|
||||
bar: { prop: 'value2' },
|
||||
baz: ['value3', 123]
|
||||
}
|
||||
};
|
||||
`,
|
||||
'example.spec.ts': `
|
||||
|
|
@ -2788,6 +2798,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
|
||||
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
||||
PLAYWRIGHT_HTML_OPEN: 'never',
|
||||
GITHUB_ACTIONS: '1',
|
||||
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
||||
GITHUB_RUN_ID: 'example-run-id',
|
||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
||||
|
|
|
|||
|
|
@ -164,6 +164,26 @@ test('should ignore test.setTimeout when debugging', async ({ runInlineTest }) =
|
|||
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 }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
|
|
|
|||
|
|
@ -21,14 +21,13 @@ test('should render html report git info metadata', async ({ runUITest }) => {
|
|||
'reporter.ts': `
|
||||
module.exports = class Reporter {
|
||||
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': `
|
||||
import { defineConfig } from '@playwright/test';
|
||||
export default defineConfig({
|
||||
populateGitInfo: true,
|
||||
reporter: './reporter.ts',
|
||||
});
|
||||
`,
|
||||
|
|
@ -37,6 +36,7 @@ test('should render html report git info metadata', async ({ runUITest }) => {
|
|||
test('should work', async ({}) => {});
|
||||
`
|
||||
}, {
|
||||
JENKINS_URL: '1',
|
||||
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 }) => {
|
||||
const { page } = await runUITest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', async () => {
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
`,
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', async () => {
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
`.trim(),
|
||||
});
|
||||
|
||||
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();
|
||||
const prompt = await page.evaluate(() => navigator.clipboard.readText());
|
||||
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.
|
||||
*/
|
||||
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
// 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 warn about missing await on expects when failing', async ({ runInlineTest }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('custom test name', async ({ page }) => {
|
||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain(warningSnippet);
|
||||
expect(stdout).toContain('the test');
|
||||
expect(stdout).toContain('custom test name');
|
||||
});
|
||||
// test.describe('await', () => {
|
||||
// test('should not care about non-API promises', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test } from '@playwright/test';
|
||||
// test('test', () => {
|
||||
// new Promise(() => {});
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// expect(exitCode).toBe(0);
|
||||
// const warnings = getWarnings(report);
|
||||
// expect(warnings.length).toEqual(0);
|
||||
// });
|
||||
|
||||
test('should warn about missing await on expects when passing', async ({ runInlineTest }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent('<div>A</div>');
|
||||
expect(page.locator('div')).toHaveText('A');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain(warningSnippet);
|
||||
});
|
||||
// test('should warn about missing await on expects when failing', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('custom test name', async ({ page }) => {
|
||||
// expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// expect(exitCode).toBe(1);
|
||||
// const warnings = getWarnings(report);
|
||||
// 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 }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).not.toContain(warningSnippet);
|
||||
});
|
||||
// test('should warn about missing await on expects when passing', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// await page.setContent('<div>A</div>');
|
||||
// expect(page.locator('div')).toHaveText('A');
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// 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 }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent('<div>A</div>');
|
||||
await expect(page.locator('div')).toHaveText('A');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).not.toContain(warningSnippet);
|
||||
});
|
||||
// test('should not warn when not missing await on expects when failing', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// await expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// expect(exitCode).toBe(1);
|
||||
// const warnings = getWarnings(report);
|
||||
// expect(warnings.length).toEqual(0);
|
||||
// });
|
||||
|
||||
test('should not warn when using then on expects when passing', async ({ runInlineTest }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent('<div>A</div>');
|
||||
expect(page.locator('div')).toHaveText('A').then(() => {});
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).not.toContain(warningSnippet);
|
||||
});
|
||||
// test('should not warn when not missing await on expects when passing', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// await page.setContent('<div>A</div>');
|
||||
// await expect(page.locator('div')).toHaveText('A');
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// expect(exitCode).toBe(0);
|
||||
// const warnings = getWarnings(report);
|
||||
// expect(warnings.length).toEqual(0);
|
||||
// });
|
||||
|
||||
test('should warn about missing await on reject', async ({ runInlineTest }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
expect(Promise.reject(new Error('foo'))).rejects.toThrow('foo');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain(warningSnippet);
|
||||
});
|
||||
// test('should not warn when using then on expects when passing', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// await page.setContent('<div>A</div>');
|
||||
// expect(page.locator('div')).toHaveText('A').then(() => {});
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// expect(exitCode).toBe(0);
|
||||
// const warnings = getWarnings(report);
|
||||
// expect(warnings.length).toEqual(0);
|
||||
// });
|
||||
|
||||
test('should warn about missing await on reject.not', async ({ runInlineTest }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
expect(Promise.reject(new Error('foo'))).rejects.not.toThrow('foo');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain(warningSnippet);
|
||||
});
|
||||
// test('should warn about missing await on reject', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// expect(Promise.reject(new Error('foo'))).rejects.toThrow('foo');
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// 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 on test.step', async ({ runInlineTest }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent('<div>A</div>');
|
||||
test.step('step', () => {});
|
||||
await expect(page.locator('div')).toHaveText('A');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain(warningSnippet);
|
||||
});
|
||||
// test('should warn about missing await on reject.not', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// expect(Promise.reject(new Error('foo'))).rejects.not.toThrow('foo');
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// expect(exitCode).toBe(1);
|
||||
// const warnings = getWarnings(report);
|
||||
// expect(warnings.length).toEqual(1);
|
||||
// expect(warnings[0].description).toContain(warningSnippet);
|
||||
// });
|
||||
|
||||
test('should not warn when not missing await on test.step', async ({ runInlineTest }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent('<div>A</div>');
|
||||
await test.step('step', () => {});
|
||||
await expect(page.locator('div')).toHaveText('A');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).not.toContain(warningSnippet);
|
||||
});
|
||||
// test('should warn about missing await on test.step', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// await page.setContent('<div>A</div>');
|
||||
// test.step('step', () => {});
|
||||
// await expect(page.locator('div')).toHaveText('A');
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// 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 on test.step.skip', async ({ runInlineTest }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent('<div>A</div>');
|
||||
test.step.skip('step', () => {});
|
||||
await expect(page.locator('div')).toHaveText('A');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain(warningSnippet);
|
||||
});
|
||||
// test('should not warn when not missing await on test.step', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// await page.setContent('<div>A</div>');
|
||||
// await test.step('step', () => {});
|
||||
// await expect(page.locator('div')).toHaveText('A');
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// expect(exitCode).toBe(0);
|
||||
// const warnings = getWarnings(report);
|
||||
// expect(warnings.length).toEqual(0);
|
||||
// });
|
||||
|
||||
test('traced promise should be instanceof Promise', async ({ runInlineTest }) => {
|
||||
const { exitCode } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent('<div>A</div>');
|
||||
const expectPromise = expect(page.locator('div')).toHaveText('A');
|
||||
expect(expectPromise instanceof Promise).toBeTruthy();
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
// test('should warn about missing await on test.step.skip', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// await page.setContent('<div>A</div>');
|
||||
// test.step.skip('step', () => {});
|
||||
// await expect(page.locator('div')).toHaveText('A');
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// 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 }) => {
|
||||
const group = ['beforeAll', 'beforeEach'];
|
||||
for (const hook of group) {
|
||||
await test.step(hook, async () => {
|
||||
const { exitCode, stdout } = 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('traced promise should be instanceof Promise', async ({ runInlineTest }) => {
|
||||
// const { exitCode } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test('test', async ({ page }) => {
|
||||
// await page.setContent('<div>A</div>');
|
||||
// const expectPromise = expect(page.locator('div')).toHaveText('A');
|
||||
// expect(expectPromise instanceof Promise).toBeTruthy();
|
||||
// });
|
||||
// `
|
||||
// });
|
||||
// expect(exitCode).toBe(0);
|
||||
// });
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain(warningSnippet);
|
||||
expect(stdout).toContain(`${group[0]}/${group[1]} hooks`);
|
||||
});
|
||||
}
|
||||
});
|
||||
// test('should warn about missing await in before hooks', async ({ runInlineTest }) => {
|
||||
// const group = ['beforeAll', 'beforeEach'];
|
||||
// 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', () => {
|
||||
const group = ['afterAll', 'afterEach'];
|
||||
for (const hook of group) {
|
||||
test(hook, async ({ runInlineTest }) => {
|
||||
const { exitCode, stdout } = 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);
|
||||
});
|
||||
`
|
||||
});
|
||||
// expect(exitCode).toBe(0);
|
||||
// const warnings = getWarnings(report);
|
||||
// expect(warnings.length).toEqual(1);
|
||||
// expect(warnings[0].description).toContain(warningSnippet);
|
||||
// expect(warnings[0].description).toContain(`${group[0]}/${group[1]} hooks`);
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain(warningSnippet);
|
||||
expect(stdout).toContain(`${group[0]}/${group[1]} hooks`);
|
||||
});
|
||||
}
|
||||
});
|
||||
// test.describe('should warn about missing await in after hooks', () => {
|
||||
// const group = ['afterAll', 'afterEach'];
|
||||
// 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 }) => {
|
||||
const { exitCode, stdout } = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test.beforeAll(async () => {
|
||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
||||
});
|
||||
test('test', async () => {
|
||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
||||
});
|
||||
test.afterEach(async () => {
|
||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain(`${warningSnippet} by the end of beforeAll/beforeEach hooks.`);
|
||||
expect(stdout).toContain(`${warningSnippet} by the end of the test.`);
|
||||
expect(stdout).toContain(`${warningSnippet} by the end of afterAll/afterEach hooks.`);
|
||||
});
|
||||
});
|
||||
// expect(exitCode).toBe(0);
|
||||
// const warnings = getWarnings(report);
|
||||
// expect(warnings.length).toEqual(1);
|
||||
// expect(warnings[0].description).toContain(warningSnippet);
|
||||
// expect(warnings[0].description).toContain(`${group[0]}/${group[1]} hooks`);
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// test('should warn about missing await across hooks and test', async ({ runInlineTest }) => {
|
||||
// const { exitCode, report } = await runInlineTest({
|
||||
// 'a.test.ts': `
|
||||
// import { test, expect } from '@playwright/test';
|
||||
// test.beforeAll(async () => {
|
||||
// expect(Promise.resolve()).resolves.toBe(undefined);
|
||||
// });
|
||||
// test('test', async () => {
|
||||
// expect(Promise.resolve()).resolves.toBe(undefined);
|
||||
// });
|
||||
// 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