Merge branch 'main' into api-request-context
Signed-off-by: Shahzad <shahzad31comp@gmail.com>
This commit is contained in:
commit
8b0a70f9bb
|
|
@ -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,7 +896,8 @@ 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.
|
||||
|
||||
|
||||
## event: APIRequestContext.apiRequest
|
||||
* since: v1.51
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
Providing `'git.commit.info': {}` property will populate it with the git commit details. This is useful for CI/CD environments.
|
||||
* 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**
|
||||
|
||||
|
|
|
|||
|
|
@ -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,23 +50,22 @@ 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' />}
|
||||
</>}
|
||||
{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'>
|
||||
{entries.map(([propertyName, value]) => {
|
||||
{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 (
|
||||
|
|
@ -104,39 +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;
|
||||
|
||||
if (info.pull_request?.link && info.pull_request?.title) {
|
||||
subject = info.pull_request?.title;
|
||||
link = info.pull_request?.link;
|
||||
}
|
||||
|
||||
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info.revision?.timestamp);
|
||||
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info.revision?.timestamp);
|
||||
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'>
|
||||
{link ? (
|
||||
<a href={link} target='_blank' rel='noopener noreferrer' title={subject}>
|
||||
{subject}
|
||||
</a>
|
||||
) : <span title={subject}>
|
||||
{subject}
|
||||
</span>}
|
||||
<a href={link} target='_blank' rel='noopener noreferrer' title={title}>{title}</a>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const GitCommitInfoView: React.FC<{ ci?: CIInfo, commit: GitCommitInfo }> = ({ ci, commit }) => {
|
||||
const title = ci?.prTitle || commit.subject;
|
||||
const link = ci?.prHref || ci?.commitHref;
|
||||
const email = ` <${commit.author.email}>`;
|
||||
const author = `${commit.author.name}${email}`;
|
||||
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(commit.committer.time);
|
||||
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(commit.committer.time);
|
||||
|
||||
return <div className='metadata-section' role='list'>
|
||||
<div role='listitem'>
|
||||
{link && <a href={link} target='_blank' rel='noopener noreferrer' title={title}>{title}</a>}
|
||||
{!link && <span title={title}>{title}</span>}
|
||||
</div>
|
||||
<div role='listitem' className='hbox'>
|
||||
<span className='mr-1'>{author}</span>
|
||||
<span title={longTimestamp}> on {shortTimestamp}</span>
|
||||
{info.ci?.link && (
|
||||
<>
|
||||
<span className='mx-2'>·</span>
|
||||
<a href={info.ci?.link} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>Logs</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
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_request?.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
|
|
@ -9268,14 +9268,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;
|
||||
|
||||
|
|
@ -9317,49 +9318,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>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -9742,12 +9701,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),
|
||||
|
|
@ -10136,55 +10089,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>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -13225,6 +13130,11 @@ export interface Locator {
|
|||
* `<article><div>Playwright</div></article>`.
|
||||
*/
|
||||
hasText?: string|RegExp;
|
||||
|
||||
/**
|
||||
* Only matches visible or invisible elements.
|
||||
*/
|
||||
visible?: boolean;
|
||||
}): Locator;
|
||||
|
||||
/**
|
||||
|
|
@ -14616,17 +14526,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.
|
||||
|
|
@ -14825,12 +14724,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.
|
||||
*
|
||||
|
|
@ -16721,12 +16614,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.
|
||||
*
|
||||
|
|
@ -17573,12 +17460,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)
|
||||
|
|
@ -17654,6 +17535,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.
|
||||
|
|
@ -17755,55 +17642,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>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -19711,7 +19550,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;
|
||||
|
||||
|
|
@ -19753,49 +19592,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>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -23292,12 +23089,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),
|
||||
|
|
@ -23653,55 +23444,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>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -407,7 +407,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),
|
||||
|
|
@ -637,7 +637,6 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
|
|||
})),
|
||||
permissions: tOptional(tArray(tString)),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
||||
offline: tOptional(tBoolean),
|
||||
httpCredentials: tOptional(tObject({
|
||||
username: tString,
|
||||
|
|
@ -725,7 +724,6 @@ scheme.BrowserNewContextParams = tObject({
|
|||
})),
|
||||
permissions: tOptional(tArray(tString)),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
||||
offline: tOptional(tBoolean),
|
||||
httpCredentials: tOptional(tObject({
|
||||
username: tString,
|
||||
|
|
@ -796,7 +794,6 @@ scheme.BrowserNewContextForReuseParams = tObject({
|
|||
})),
|
||||
permissions: tOptional(tArray(tString)),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
|
||||
offline: tOptional(tBoolean),
|
||||
httpCredentials: tOptional(tObject({
|
||||
username: tString,
|
||||
|
|
@ -2704,7 +2701,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: []
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -213,7 +213,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) {
|
||||
|
|
@ -610,7 +610,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,
|
||||
|
|
@ -662,7 +662,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,
|
||||
|
|
@ -695,7 +695,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}`);
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
301
packages/playwright-core/types/types.d.ts
vendored
301
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -9268,14 +9268,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;
|
||||
|
||||
|
|
@ -9317,49 +9318,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>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -9742,12 +9701,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),
|
||||
|
|
@ -10136,55 +10089,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>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -13225,6 +13130,11 @@ export interface Locator {
|
|||
* `<article><div>Playwright</div></article>`.
|
||||
*/
|
||||
hasText?: string|RegExp;
|
||||
|
||||
/**
|
||||
* Only matches visible or invisible elements.
|
||||
*/
|
||||
visible?: boolean;
|
||||
}): Locator;
|
||||
|
||||
/**
|
||||
|
|
@ -14616,17 +14526,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.
|
||||
|
|
@ -14825,12 +14724,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.
|
||||
*
|
||||
|
|
@ -16721,12 +16614,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.
|
||||
*
|
||||
|
|
@ -17573,12 +17460,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)
|
||||
|
|
@ -17654,6 +17535,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.
|
||||
|
|
@ -17755,55 +17642,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>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -19711,7 +19550,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;
|
||||
|
||||
|
|
@ -19753,49 +19592,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>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -23292,12 +23089,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),
|
||||
|
|
@ -23653,55 +23444,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>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
59
packages/playwright/src/isomorphic/types.d.ts
vendored
59
packages/playwright/src/isomorphic/types.d.ts
vendored
|
|
@ -14,23 +14,42 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface GitCommitInfo {
|
||||
revision?: {
|
||||
id?: string;
|
||||
author?: string;
|
||||
email?: string;
|
||||
subject?: string;
|
||||
timestamp?: number;
|
||||
link?: string;
|
||||
diff?: string;
|
||||
},
|
||||
pull_request?: {
|
||||
link?: string;
|
||||
diff?: string;
|
||||
base?: string;
|
||||
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,119 +14,151 @@
|
|||
* 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) => {
|
||||
const commitProperty = fullConfig.config.metadata['git.commit.info'];
|
||||
if (commitProperty && typeof commitProperty === 'object' && Object.keys(commitProperty).length === 0)
|
||||
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 commitInfo = await linksFromEnv();
|
||||
await enrichStatusFromCLI(options?.directory || configDir, commitInfo);
|
||||
config.metadata = config.metadata || {};
|
||||
config.metadata['git.commit.info'] = commitInfo;
|
||||
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(): Promise<GitCommitInfo> {
|
||||
const out: GitCommitInfo = {};
|
||||
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
||||
if (process.env.BUILD_URL) {
|
||||
out.ci = out.ci || {};
|
||||
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 = out.revision || {};
|
||||
out.revision.link = `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`;
|
||||
}
|
||||
if (process.env.CI_JOB_URL) {
|
||||
out.ci = out.ci || {};
|
||||
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 = out.revision || {};
|
||||
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 = out.ci || {};
|
||||
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_request = out.pull_request || {};
|
||||
out.pull_request.title = json.pull_request.title;
|
||||
out.pull_request.link = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${json.pull_request.number}`;
|
||||
out.pull_request.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 enrichStatusFromCLI(gitDir: string, commitInfo: GitCommitInfo) {
|
||||
const separator = `:${createGuid().slice(0, 4)}:`;
|
||||
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 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 }
|
||||
`git log -1 --pretty=format:"${tokens.join(separator)}" && git rev-parse --abbrev-ref HEAD`, [],
|
||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS, shell: true }
|
||||
);
|
||||
if (commitInfoResult.code)
|
||||
return;
|
||||
return undefined;
|
||||
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;
|
||||
const [hash, shortHash, subject, body, authorName, authorEmail, authorTime, committerName, committerEmail, committerTime, branch] = showOutput.split(separator);
|
||||
|
||||
commitInfo.revision = {
|
||||
...commitInfo.revision,
|
||||
id,
|
||||
author,
|
||||
email,
|
||||
return {
|
||||
shortHash,
|
||||
hash,
|
||||
subject,
|
||||
timestamp,
|
||||
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 (commitInfo.pull_request?.base) {
|
||||
const pullDiffResult = await spawnAsync(
|
||||
'git',
|
||||
['diff', commitInfo.pull_request?.base],
|
||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||
);
|
||||
if (!pullDiffResult.code)
|
||||
commitInfo.pull_request!.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)
|
||||
commitInfo.revision!.diff = diffResult.stdout.substring(0, diffLimit);
|
||||
}
|
||||
}
|
||||
|
||||
async function gitDiff(gitDir: string, ci?: CIInfo): Promise<string | undefined> {
|
||||
const diffLimit = 100_000;
|
||||
const baseHash = ci?.baseHash ?? 'HEAD~1';
|
||||
|
||||
const pullDiffResult = await spawnAsync(
|
||||
'git',
|
||||
['diff', baseHash],
|
||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||
);
|
||||
if (!pullDiffResult.code)
|
||||
return pullDiffResult.stdout.substring(0, diffLimit);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,4 +35,3 @@ export type TestRunnerPluginRegistration = {
|
|||
};
|
||||
|
||||
export { webServer } from './webServerPlugin';
|
||||
export { gitCommitInfo } from './gitCommitInfoPlugin';
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -330,8 +330,8 @@ export class WorkerMain extends ProcessRunner {
|
|||
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 +356,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 +376,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 +393,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 +448,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 +481,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 +534,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 +587,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) {
|
||||
|
|
|
|||
12
packages/playwright/types/test.d.ts
vendored
12
packages/playwright/types/test.d.ts
vendored
|
|
@ -1284,9 +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.
|
||||
*
|
||||
* Providing `'git.commit.info': {}` property will populate it with the git commit details. This is useful for CI/CD
|
||||
* environments.
|
||||
* On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic
|
||||
* generation.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
|
|
@ -8791,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
|
||||
|
|
|
|||
12
packages/protocol/src/channels.d.ts
vendored
12
packages/protocol/src/channels.d.ts
vendored
|
|
@ -664,7 +664,7 @@ export type PlaywrightNewRequestParams = {
|
|||
userAgent?: string,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
failOnStatusCode?: boolean,
|
||||
clientCertificates?: {
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
|
|
@ -696,7 +696,7 @@ export type PlaywrightNewRequestOptions = {
|
|||
userAgent?: string,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
failOnStatusCode?: boolean,
|
||||
clientCertificates?: {
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
|
|
@ -1070,7 +1070,6 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1152,7 +1151,6 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1269,7 +1267,6 @@ export type BrowserNewContextParams = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1337,7 +1334,6 @@ export type BrowserNewContextOptions = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1408,7 +1404,6 @@ export type BrowserNewContextForReuseParams = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -1476,7 +1471,6 @@ export type BrowserNewContextForReuseOptions = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -4843,7 +4837,6 @@ export type AndroidDeviceLaunchBrowserParams = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -4909,7 +4902,6 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
|||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
apiRequestFailsOnErrorStatus?: boolean,
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
|
|||
|
|
@ -567,7 +567,6 @@ ContextOptions:
|
|||
extraHTTPHeaders:
|
||||
type: array?
|
||||
items: NameValue
|
||||
apiRequestFailsOnErrorStatus: boolean?
|
||||
offline: boolean?
|
||||
httpCredentials:
|
||||
type: object?
|
||||
|
|
@ -799,7 +798,7 @@ Playwright:
|
|||
extraHTTPHeaders:
|
||||
type: array?
|
||||
items: NameValue
|
||||
apiRequestFailsOnErrorStatus: boolean?
|
||||
failOnStatusCode: boolean?
|
||||
clientCertificates:
|
||||
type: array?
|
||||
items:
|
||||
|
|
|
|||
|
|
@ -24,20 +24,20 @@ 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';
|
||||
|
||||
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[]) {
|
||||
|
|
@ -100,8 +100,7 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined):
|
|||
function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSource }: { message: string, error: ErrorDescription, errorId: string, sdkLanguage: Language, pageSnapshot?: string, revealInSource: (error: ErrorDescription) => void }) {
|
||||
const [showLLM, setShowLLM] = React.useState(false);
|
||||
const llmAvailable = useIsLLMAvailable();
|
||||
const gitCommitInfo = useGitCommitInfo();
|
||||
const diff = gitCommitInfo?.pull_request?.diff ?? gitCommitInfo?.revision?.diff;
|
||||
const metadata = useCommitInfo();
|
||||
|
||||
let location: string | undefined;
|
||||
let longLocation: string | undefined;
|
||||
|
|
@ -127,8 +126,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} pageSnapshot={pageSnapshot} diff={metadata?.gitDiff} />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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")`,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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,12 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
]);
|
||||
});
|
||||
|
||||
test('should include metadata with git.commit.info', 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 {
|
||||
metadata: { 'git.commit.info': {}, foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
||||
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
||||
};
|
||||
`,
|
||||
'example.spec.ts': `
|
||||
|
|
@ -1219,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',
|
||||
|
|
@ -1240,12 +1241,12 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
`);
|
||||
});
|
||||
|
||||
test('should include metadata with git.commit.info 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 {
|
||||
metadata: { 'git.commit.info': {}, foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
||||
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
||||
};
|
||||
`,
|
||||
'example.spec.ts': `
|
||||
|
|
@ -1281,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',
|
||||
|
|
@ -1296,9 +1298,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
- list:
|
||||
- listitem:
|
||||
- link "My PR"
|
||||
- listitem:
|
||||
- text: /William <shakespeare@example\\.local>/
|
||||
- link "Logs"
|
||||
- listitem: /William <shakespeare@example.local>/
|
||||
- list:
|
||||
- listitem: "foo : value1"
|
||||
- listitem: "bar : {\\"prop\\":\\"value2\\"}"
|
||||
|
|
@ -1306,7 +1306,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
`);
|
||||
});
|
||||
|
||||
test('should not include git metadata w/o git.commit.info', 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 {};
|
||||
|
|
@ -1330,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' } }
|
||||
},
|
||||
};
|
||||
`,
|
||||
|
|
@ -2765,7 +2765,6 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
'playwright.config.ts': `
|
||||
export default {
|
||||
metadata: {
|
||||
'git.commit.info': {},
|
||||
foo: 'value1',
|
||||
bar: { prop: 'value2' },
|
||||
baz: ['value3', 123]
|
||||
|
|
@ -2799,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',
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
metadata: { 'git.commit.info': {} },
|
||||
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',
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue