feat(test-runner): implement expect(pageOrLocator).toHaveScreenshot (#12242)
Fixes #9938
This commit is contained in:
parent
8d94ed134c
commit
396d920145
|
@ -968,6 +968,35 @@ Property value.
|
||||||
|
|
||||||
### option: LocatorAssertions.toHaveJSProperty.timeout = %%-assertions-timeout-%%
|
### option: LocatorAssertions.toHaveJSProperty.timeout = %%-assertions-timeout-%%
|
||||||
|
|
||||||
|
## async method: LocatorAssertions.toHaveScreenshot
|
||||||
|
* langs: js
|
||||||
|
|
||||||
|
Ensures that [Locator] resolves to a given screenshot. This function will re-take
|
||||||
|
screenshots until it matches with the saved expectation.
|
||||||
|
|
||||||
|
If there's no expectation yet, it will wait until two consecutive screenshots
|
||||||
|
yield the same result, and save the last one as an expectation.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const locator = page.locator('button');
|
||||||
|
await expect(locator).toHaveScreenshot();
|
||||||
|
```
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveScreenshot.timeout = %%-assertions-timeout-%%
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveScreenshot.disableAnimations = %%-screenshot-option-disable-animations-%%
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveScreenshot.omitBackground = %%-screenshot-option-omit-background-%%
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveScreenshot.mask = %%-screenshot-option-mask-%%
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveScreenshot.pixelCount = %%-assertions-pixel-count-%%
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveScreenshot.pixelRatio = %%-assertions-pixel-ratio-%%
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveScreenshot.threshold = %%-assertions-threshold-%%
|
||||||
|
|
||||||
|
|
||||||
## async method: LocatorAssertions.toHaveText
|
## async method: LocatorAssertions.toHaveText
|
||||||
* langs:
|
* langs:
|
||||||
- alias-java: hasText
|
- alias-java: hasText
|
||||||
|
|
|
@ -2650,21 +2650,9 @@ Returns the buffer with the captured screenshot.
|
||||||
|
|
||||||
### option: Page.screenshot.-inline- = %%-screenshot-options-common-list-%%
|
### option: Page.screenshot.-inline- = %%-screenshot-options-common-list-%%
|
||||||
|
|
||||||
### option: Page.screenshot.fullPage
|
### option: Page.screenshot.fullPage = %%-screenshot-option-full-page-%%
|
||||||
- `fullPage` <[boolean]>
|
|
||||||
|
|
||||||
When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to
|
### option: Page.screenshot.clip = %%-screenshot-option-clip-%%
|
||||||
`false`.
|
|
||||||
|
|
||||||
|
|
||||||
### option: Page.screenshot.clip
|
|
||||||
- `clip` <[Object]>
|
|
||||||
- `x` <[float]> x-coordinate of top-left corner of clip area
|
|
||||||
- `y` <[float]> y-coordinate of top-left corner of clip area
|
|
||||||
- `width` <[float]> width of clipping area
|
|
||||||
- `height` <[float]> height of clipping area
|
|
||||||
|
|
||||||
An object which specifies clipping of the resulting image. Should have the following fields:
|
|
||||||
|
|
||||||
## async method: Page.selectOption
|
## async method: Page.selectOption
|
||||||
- returns: <[Array]<[string]>>
|
- returns: <[Array]<[string]>>
|
||||||
|
|
|
@ -114,6 +114,37 @@ Expected substring or RegExp.
|
||||||
|
|
||||||
### option: PageAssertions.NotToHaveURL.timeout = %%-assertions-timeout-%%
|
### option: PageAssertions.NotToHaveURL.timeout = %%-assertions-timeout-%%
|
||||||
|
|
||||||
|
## async method: PageAssertions.toHaveScreenshot
|
||||||
|
* langs: js
|
||||||
|
|
||||||
|
Ensures that the page resolves to a given screenshot. This function will re-take
|
||||||
|
screenshots until it matches with the saved expectation.
|
||||||
|
|
||||||
|
If there's no expectation yet, it will wait until two consecutive screenshots
|
||||||
|
yield the same result, and save the last one as an expectation.
|
||||||
|
|
||||||
|
```js
|
||||||
|
await expect(page).toHaveScreenshot();
|
||||||
|
```
|
||||||
|
|
||||||
|
### option: PageAssertions.toHaveScreenshot.timeout = %%-assertions-timeout-%%
|
||||||
|
|
||||||
|
### option: PageAssertions.toHaveScreenshot.disableAnimations = %%-screenshot-option-disable-animations-%%
|
||||||
|
|
||||||
|
### option: PageAssertions.toHaveScreenshot.omitBackground = %%-screenshot-option-omit-background-%%
|
||||||
|
|
||||||
|
### option: PageAssertions.toHaveScreenshot.fullPage = %%-screenshot-option-full-page-%%
|
||||||
|
|
||||||
|
### option: PageAssertions.toHaveScreenshot.clip = %%-screenshot-option-clip-%%
|
||||||
|
|
||||||
|
### option: PageAssertions.toHaveScreenshot.mask = %%-screenshot-option-mask-%%
|
||||||
|
|
||||||
|
### option: PageAssertions.toHaveScreenshot.pixelCount = %%-assertions-pixel-count-%%
|
||||||
|
|
||||||
|
### option: PageAssertions.toHaveScreenshot.pixelRatio = %%-assertions-pixel-ratio-%%
|
||||||
|
|
||||||
|
### option: PageAssertions.toHaveScreenshot.threshold = %%-assertions-threshold-%%
|
||||||
|
|
||||||
## async method: PageAssertions.toHaveTitle
|
## async method: PageAssertions.toHaveTitle
|
||||||
* langs:
|
* langs:
|
||||||
- alias-java: hasTitle
|
- alias-java: hasTitle
|
||||||
|
@ -194,4 +225,4 @@ await Expect(page).ToHaveURL(new Regex(".*checkout"));
|
||||||
|
|
||||||
Expected substring or RegExp.
|
Expected substring or RegExp.
|
||||||
|
|
||||||
### option: PageAssertions.toHaveURL.timeout = %%-assertions-timeout-%%
|
### option: PageAssertions.toHaveURL.timeout = %%-assertions-timeout-%%
|
||||||
|
|
|
@ -709,6 +709,19 @@ using the [`method: AndroidDevice.setDefaultTimeout`] method.
|
||||||
|
|
||||||
Time to retry the assertion for. Defaults to `timeout` in [`property: TestConfig.expect`].
|
Time to retry the assertion for. Defaults to `timeout` in [`property: TestConfig.expect`].
|
||||||
|
|
||||||
|
## assertions-pixel-count
|
||||||
|
* langs: js
|
||||||
|
- `pixelCount` <[int]> an acceptable amount of pixels that could be different, unset by default.
|
||||||
|
|
||||||
|
## assertions-pixel-ratio
|
||||||
|
* langs: js
|
||||||
|
- `pixelRatio` <[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`, unset by default.
|
||||||
|
|
||||||
|
## assertions-threshold
|
||||||
|
* langs: js
|
||||||
|
- `threshold` <[float]> an acceptable percieved color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax), default is configurable with [`property: TestConfig.expect`]. Defaults to `0.2`.
|
||||||
|
|
||||||
|
|
||||||
## assertions-timeout
|
## assertions-timeout
|
||||||
* langs: java, python, csharp
|
* langs: java, python, csharp
|
||||||
- `timeout` <[float]>
|
- `timeout` <[float]>
|
||||||
|
@ -919,6 +932,21 @@ Specify screenshot type, defaults to `png`.
|
||||||
Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with
|
Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with
|
||||||
a pink box `#FF00FF` that completely covers its bounding box.
|
a pink box `#FF00FF` that completely covers its bounding box.
|
||||||
|
|
||||||
|
## screenshot-option-full-page
|
||||||
|
- `fullPage` <[boolean]>
|
||||||
|
|
||||||
|
When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to
|
||||||
|
`false`.
|
||||||
|
|
||||||
|
## screenshot-option-clip
|
||||||
|
- `clip` <[Object]>
|
||||||
|
- `x` <[float]> x-coordinate of top-left corner of clip area
|
||||||
|
- `y` <[float]> y-coordinate of top-left corner of clip area
|
||||||
|
- `width` <[float]> width of clipping area
|
||||||
|
- `height` <[float]> height of clipping area
|
||||||
|
|
||||||
|
An object which specifies clipping of the resulting image. Should have the following fields:
|
||||||
|
|
||||||
## screenshot-options-common-list
|
## screenshot-options-common-list
|
||||||
- %%-screenshot-option-disable-animations-%%
|
- %%-screenshot-option-disable-animations-%%
|
||||||
- %%-screenshot-option-omit-background-%%
|
- %%-screenshot-option-omit-background-%%
|
||||||
|
|
|
@ -349,7 +349,7 @@ await expect(page).toHaveURL(/.*checkout/);
|
||||||
- `options`
|
- `options`
|
||||||
- `threshold` <[float]> an acceptable percieved color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax), default is configurable with [`property: TestConfig.expect`]. Defaults to `0.2`.
|
- `threshold` <[float]> an acceptable percieved color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax), default is configurable with [`property: TestConfig.expect`]. Defaults to `0.2`.
|
||||||
- `pixelCount` <[int]> an acceptable amount of pixels that could be different, unset by default.
|
- `pixelCount` <[int]> an acceptable amount of pixels that could be different, unset by default.
|
||||||
- `pixelRatio` <[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
|
- `pixelRatio` <[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`, unset by default.
|
||||||
|
|
||||||
Ensures that passed value, either a [string] or a [Buffer], matches the expected snapshot stored in the test snapshots directory.
|
Ensures that passed value, either a [string] or a [Buffer], matches the expected snapshot stored in the test snapshots directory.
|
||||||
|
|
||||||
|
@ -366,3 +366,37 @@ expect(await page.screenshot()).toMatchSnapshot(['landing', 'step3.png']);
|
||||||
```
|
```
|
||||||
|
|
||||||
Learn more about [visual comparisons](./test-snapshots.md).
|
Learn more about [visual comparisons](./test-snapshots.md).
|
||||||
|
|
||||||
|
## expect(pageOrLocator).toHaveScreenshot([options])
|
||||||
|
- `options`
|
||||||
|
- `name` <[string] | [Array]<[string]>> Optional snapshot name.
|
||||||
|
- `disableAnimations` <[boolean]> When true, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on their duration:
|
||||||
|
- finite animations are fast-forwarded to completion, so they'll fire `transitionend` event.
|
||||||
|
- infinite animations are canceled to initial state, and then played over after the screenshot.
|
||||||
|
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
|
||||||
|
- `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to `false`.
|
||||||
|
- `mask` <[Array]<[Locator]>> Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with
|
||||||
|
a pink box `#FF00FF` that completely covers its bounding box.
|
||||||
|
- `clip` <[Object]> An object which specifies clipping of the resulting image.
|
||||||
|
- `x` <[float]> x-coordinate of top-left corner of clip area
|
||||||
|
- `y` <[float]> y-coordinate of top-left corner of clip area
|
||||||
|
- `width` <[float]> width of clipping area
|
||||||
|
- `height` <[float]> height of clipping area
|
||||||
|
- `threshold` <[float]> an acceptable percieved color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax), default is configurable with [`property: TestConfig.expect`]. Defaults to `0.2`.
|
||||||
|
- `pixelCount` <[int]> an acceptable amount of pixels that could be different, unset by default.
|
||||||
|
- `pixelRatio` <[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`, unset by default.
|
||||||
|
- `timeout` <[number]> Time to retry assertion for, defaults to `timeout` in [`property: TestConfig.expect`].
|
||||||
|
|
||||||
|
Ensures that passed value, either a [string] or a [Buffer], matches the expected snapshot stored in the test snapshots directory.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Basic usage.
|
||||||
|
await expect(page).toHaveScreenshot({ name: 'landing-page.png' });
|
||||||
|
await expect(page.locator('text=Submit')).toHaveScreenshot();
|
||||||
|
|
||||||
|
// Take a full page screenshot and auto-generate screenshot name
|
||||||
|
await expect(page).toHaveScreenshot({ fullPage: true });
|
||||||
|
|
||||||
|
// Configure image matching properties.
|
||||||
|
await expect(page.locator('text=Submit').toHaveScreenshot({ pixelRatio: 0.01 });
|
||||||
|
```
|
||||||
|
|
|
@ -62,6 +62,13 @@ type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> &
|
||||||
};
|
};
|
||||||
type Listener = (...args: any[]) => void;
|
type Listener = (...args: any[]) => void;
|
||||||
|
|
||||||
|
type ExpectScreenshotOptions = Omit<channels.PageExpectScreenshotOptions, 'screenshotOptions' | 'locator' | 'expected'> & {
|
||||||
|
expected?: Buffer,
|
||||||
|
locator?: Locator,
|
||||||
|
isNot: boolean,
|
||||||
|
screenshotOptions: Omit<channels.PageExpectScreenshotOptions['screenshotOptions'], 'mask'> & { mask?: Locator[] }
|
||||||
|
};
|
||||||
|
|
||||||
export class Page extends ChannelOwner<channels.PageChannel> implements api.Page {
|
export class Page extends ChannelOwner<channels.PageChannel> implements api.Page {
|
||||||
private _browserContext: BrowserContext;
|
private _browserContext: BrowserContext;
|
||||||
_ownedContext: BrowserContext | undefined;
|
_ownedContext: BrowserContext | undefined;
|
||||||
|
@ -476,6 +483,36 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[]}> {
|
||||||
|
const mask = options.screenshotOptions?.mask ? options.screenshotOptions?.mask.map(locator => ({
|
||||||
|
frame: locator._frame._channel,
|
||||||
|
selector: locator._selector,
|
||||||
|
})) : undefined;
|
||||||
|
const locator = options.locator ? {
|
||||||
|
frame: options.locator._frame._channel,
|
||||||
|
selector: options.locator._selector,
|
||||||
|
} : undefined;
|
||||||
|
const expected = options.expected ? options.expected.toString('base64') : undefined;
|
||||||
|
|
||||||
|
const result = await this._channel.expectScreenshot({
|
||||||
|
...options,
|
||||||
|
isNot: !!options.isNot,
|
||||||
|
expected,
|
||||||
|
locator,
|
||||||
|
screenshotOptions: {
|
||||||
|
...options.screenshotOptions,
|
||||||
|
mask,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
log: result.log,
|
||||||
|
actual: result.actual ? Buffer.from(result.actual, 'base64') : undefined,
|
||||||
|
previous: result.previous ? Buffer.from(result.previous, 'base64') : undefined,
|
||||||
|
diff: result.diff ? Buffer.from(result.diff, 'base64') : undefined,
|
||||||
|
errorMessage: result.errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async title(): Promise<string> {
|
async title(): Promise<string> {
|
||||||
return this._mainFrame.title();
|
return this._mainFrame.title();
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,6 +150,34 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel> imple
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async expectScreenshot(params: channels.PageExpectScreenshotParams, metadata: CallMetadata): Promise<channels.PageExpectScreenshotResult> {
|
||||||
|
const mask: { frame: Frame, selector: string }[] = (params.screenshotOptions?.mask || []).map(({ frame, selector }) => ({
|
||||||
|
frame: (frame as FrameDispatcher)._object,
|
||||||
|
selector,
|
||||||
|
}));
|
||||||
|
const locator: { frame: Frame, selector: string } | undefined = params.locator ? {
|
||||||
|
frame: (params.locator.frame as FrameDispatcher)._object,
|
||||||
|
selector: params.locator.selector,
|
||||||
|
} : undefined;
|
||||||
|
const expected = params.expected ? Buffer.from(params.expected, 'base64') : undefined;
|
||||||
|
const result = await this._page.expectScreenshot(metadata, {
|
||||||
|
...params,
|
||||||
|
expected,
|
||||||
|
locator,
|
||||||
|
screenshotOptions: {
|
||||||
|
...params.screenshotOptions,
|
||||||
|
mask,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
diff: result.diff?.toString('base64'),
|
||||||
|
errorMessage: result.errorMessage,
|
||||||
|
actual: result.actual?.toString('base64'),
|
||||||
|
previous: result.previous?.toString('base64'),
|
||||||
|
log: result.log,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async screenshot(params: channels.PageScreenshotParams, metadata: CallMetadata): Promise<channels.PageScreenshotResult> {
|
async screenshot(params: channels.PageScreenshotParams, metadata: CallMetadata): Promise<channels.PageScreenshotResult> {
|
||||||
const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({
|
const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({
|
||||||
frame: (frame as FrameDispatcher)._object,
|
frame: (frame as FrameDispatcher)._object,
|
||||||
|
|
|
@ -1330,6 +1330,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
|
||||||
goBack(params: PageGoBackParams, metadata?: Metadata): Promise<PageGoBackResult>;
|
goBack(params: PageGoBackParams, metadata?: Metadata): Promise<PageGoBackResult>;
|
||||||
goForward(params: PageGoForwardParams, metadata?: Metadata): Promise<PageGoForwardResult>;
|
goForward(params: PageGoForwardParams, metadata?: Metadata): Promise<PageGoForwardResult>;
|
||||||
reload(params: PageReloadParams, metadata?: Metadata): Promise<PageReloadResult>;
|
reload(params: PageReloadParams, metadata?: Metadata): Promise<PageReloadResult>;
|
||||||
|
expectScreenshot(params: PageExpectScreenshotParams, metadata?: Metadata): Promise<PageExpectScreenshotResult>;
|
||||||
screenshot(params: PageScreenshotParams, metadata?: Metadata): Promise<PageScreenshotResult>;
|
screenshot(params: PageScreenshotParams, metadata?: Metadata): Promise<PageScreenshotResult>;
|
||||||
setExtraHTTPHeaders(params: PageSetExtraHTTPHeadersParams, metadata?: Metadata): Promise<PageSetExtraHTTPHeadersResult>;
|
setExtraHTTPHeaders(params: PageSetExtraHTTPHeadersParams, metadata?: Metadata): Promise<PageSetExtraHTTPHeadersResult>;
|
||||||
setNetworkInterceptionEnabled(params: PageSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise<PageSetNetworkInterceptionEnabledResult>;
|
setNetworkInterceptionEnabled(params: PageSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise<PageSetNetworkInterceptionEnabledResult>;
|
||||||
|
@ -1486,6 +1487,60 @@ export type PageReloadOptions = {
|
||||||
export type PageReloadResult = {
|
export type PageReloadResult = {
|
||||||
response?: ResponseChannel,
|
response?: ResponseChannel,
|
||||||
};
|
};
|
||||||
|
export type PageExpectScreenshotParams = {
|
||||||
|
expected?: Binary,
|
||||||
|
timeout?: number,
|
||||||
|
isNot: boolean,
|
||||||
|
locator?: {
|
||||||
|
frame: FrameChannel,
|
||||||
|
selector: string,
|
||||||
|
},
|
||||||
|
comparatorOptions?: {
|
||||||
|
pixelCount?: number,
|
||||||
|
pixelRatio?: number,
|
||||||
|
threshold?: number,
|
||||||
|
},
|
||||||
|
screenshotOptions?: {
|
||||||
|
omitBackground?: boolean,
|
||||||
|
fullPage?: boolean,
|
||||||
|
disableAnimations?: boolean,
|
||||||
|
clip?: Rect,
|
||||||
|
mask?: {
|
||||||
|
frame: FrameChannel,
|
||||||
|
selector: string,
|
||||||
|
}[],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export type PageExpectScreenshotOptions = {
|
||||||
|
expected?: Binary,
|
||||||
|
timeout?: number,
|
||||||
|
locator?: {
|
||||||
|
frame: FrameChannel,
|
||||||
|
selector: string,
|
||||||
|
},
|
||||||
|
comparatorOptions?: {
|
||||||
|
pixelCount?: number,
|
||||||
|
pixelRatio?: number,
|
||||||
|
threshold?: number,
|
||||||
|
},
|
||||||
|
screenshotOptions?: {
|
||||||
|
omitBackground?: boolean,
|
||||||
|
fullPage?: boolean,
|
||||||
|
disableAnimations?: boolean,
|
||||||
|
clip?: Rect,
|
||||||
|
mask?: {
|
||||||
|
frame: FrameChannel,
|
||||||
|
selector: string,
|
||||||
|
}[],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export type PageExpectScreenshotResult = {
|
||||||
|
diff?: Binary,
|
||||||
|
errorMessage?: string,
|
||||||
|
actual?: Binary,
|
||||||
|
previous?: Binary,
|
||||||
|
log?: string[],
|
||||||
|
};
|
||||||
export type PageScreenshotParams = {
|
export type PageScreenshotParams = {
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
type?: 'png' | 'jpeg',
|
type?: 'png' | 'jpeg',
|
||||||
|
|
|
@ -990,6 +990,45 @@ Page:
|
||||||
tracing:
|
tracing:
|
||||||
snapshot: true
|
snapshot: true
|
||||||
|
|
||||||
|
expectScreenshot:
|
||||||
|
parameters:
|
||||||
|
expected: binary?
|
||||||
|
timeout: number?
|
||||||
|
isNot: boolean
|
||||||
|
locator:
|
||||||
|
type: object?
|
||||||
|
properties:
|
||||||
|
frame: Frame
|
||||||
|
selector: string
|
||||||
|
comparatorOptions:
|
||||||
|
type: object?
|
||||||
|
properties:
|
||||||
|
pixelCount: number?
|
||||||
|
pixelRatio: number?
|
||||||
|
threshold: number?
|
||||||
|
screenshotOptions:
|
||||||
|
type: object?
|
||||||
|
properties:
|
||||||
|
omitBackground: boolean?
|
||||||
|
fullPage: boolean?
|
||||||
|
disableAnimations: boolean?
|
||||||
|
clip: Rect?
|
||||||
|
mask:
|
||||||
|
type: array?
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
frame: Frame
|
||||||
|
selector: string
|
||||||
|
returns:
|
||||||
|
diff: binary?
|
||||||
|
errorMessage: string?
|
||||||
|
actual: binary?
|
||||||
|
previous: binary?
|
||||||
|
log:
|
||||||
|
type: array?
|
||||||
|
items: string
|
||||||
|
|
||||||
screenshot:
|
screenshot:
|
||||||
parameters:
|
parameters:
|
||||||
timeout: number?
|
timeout: number?
|
||||||
|
|
|
@ -540,6 +540,30 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
timeout: tOptional(tNumber),
|
timeout: tOptional(tNumber),
|
||||||
waitUntil: tOptional(tType('LifecycleEvent')),
|
waitUntil: tOptional(tType('LifecycleEvent')),
|
||||||
});
|
});
|
||||||
|
scheme.PageExpectScreenshotParams = tObject({
|
||||||
|
expected: tOptional(tBinary),
|
||||||
|
timeout: tOptional(tNumber),
|
||||||
|
isNot: tBoolean,
|
||||||
|
locator: tOptional(tObject({
|
||||||
|
frame: tChannel('Frame'),
|
||||||
|
selector: tString,
|
||||||
|
})),
|
||||||
|
comparatorOptions: tOptional(tObject({
|
||||||
|
pixelCount: tOptional(tNumber),
|
||||||
|
pixelRatio: tOptional(tNumber),
|
||||||
|
threshold: tOptional(tNumber),
|
||||||
|
})),
|
||||||
|
screenshotOptions: tOptional(tObject({
|
||||||
|
omitBackground: tOptional(tBoolean),
|
||||||
|
fullPage: tOptional(tBoolean),
|
||||||
|
disableAnimations: tOptional(tBoolean),
|
||||||
|
clip: tOptional(tType('Rect')),
|
||||||
|
mask: tOptional(tArray(tObject({
|
||||||
|
frame: tChannel('Frame'),
|
||||||
|
selector: tString,
|
||||||
|
}))),
|
||||||
|
})),
|
||||||
|
});
|
||||||
scheme.PageScreenshotParams = tObject({
|
scheme.PageScreenshotParams = tObject({
|
||||||
timeout: tOptional(tNumber),
|
timeout: tOptional(tNumber),
|
||||||
type: tOptional(tEnum(['png', 'jpeg'])),
|
type: tOptional(tEnum(['png', 'jpeg'])),
|
||||||
|
|
|
@ -119,7 +119,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
declare readonly _context: FrameExecutionContext;
|
declare readonly _context: FrameExecutionContext;
|
||||||
readonly _page: Page;
|
readonly _page: Page;
|
||||||
declare readonly _objectId: string;
|
declare readonly _objectId: string;
|
||||||
private _frame: frames.Frame;
|
readonly _frame: frames.Frame;
|
||||||
|
|
||||||
constructor(context: FrameExecutionContext, objectId: string) {
|
constructor(context: FrameExecutionContext, objectId: string) {
|
||||||
super(context, 'node', undefined, objectId);
|
super(context, 'node', undefined, objectId);
|
||||||
|
|
|
@ -36,6 +36,7 @@ import type { ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll,
|
||||||
import { isSessionClosedError } from './protocolError';
|
import { isSessionClosedError } from './protocolError';
|
||||||
import { isInvalidSelectorError, splitSelectorByFrame, stringifySelector, ParsedSelector } from './common/selectorParser';
|
import { isInvalidSelectorError, splitSelectorByFrame, stringifySelector, ParsedSelector } from './common/selectorParser';
|
||||||
import { SelectorInfo } from './selectors';
|
import { SelectorInfo } from './selectors';
|
||||||
|
import { ScreenshotOptions } from './screenshotter';
|
||||||
|
|
||||||
type ContextData = {
|
type ContextData = {
|
||||||
contextPromise: ManualPromise<dom.FrameExecutionContext | Error>;
|
contextPromise: ManualPromise<dom.FrameExecutionContext | Error>;
|
||||||
|
@ -1057,6 +1058,13 @@ export class Frame extends SdkObject {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rafrafTimeoutScreenshotElementWithProgress(progress: Progress, selector: string, timeout: number, options: ScreenshotOptions): Promise<Buffer|undefined> {
|
||||||
|
return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, async handle => {
|
||||||
|
await handle._frame.rafrafTimeout(timeout);
|
||||||
|
return await this._page._screenshotter.screenshotElement(progress, handle, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async click(metadata: CallMetadata, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
async click(metadata: CallMetadata, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
@ -1371,6 +1379,21 @@ export class Frame extends SdkObject {
|
||||||
return context.evaluate(() => document.title);
|
return context.evaluate(() => document.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rafrafTimeout(timeout: number): Promise<void> {
|
||||||
|
if (timeout === 0)
|
||||||
|
return;
|
||||||
|
const context = await this._utilityContext();
|
||||||
|
await Promise.all([
|
||||||
|
// wait for double raf
|
||||||
|
context.evaluate(() => new Promise(x => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(x);
|
||||||
|
});
|
||||||
|
})),
|
||||||
|
new Promise(fulfill => setTimeout(fulfill, timeout)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
_onDetached() {
|
_onDetached() {
|
||||||
this._stopNetworkIdleTimer();
|
this._stopNetworkIdleTimer();
|
||||||
this._detached = true;
|
this._detached = true;
|
||||||
|
|
|
@ -31,11 +31,12 @@ import { Progress, ProgressController } from './progress';
|
||||||
import { assert, isError } from '../utils/utils';
|
import { assert, isError } from '../utils/utils';
|
||||||
import { ManualPromise } from '../utils/async';
|
import { ManualPromise } from '../utils/async';
|
||||||
import { debugLogger } from '../utils/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
|
import { mimeTypeToComparator, ImageComparatorOptions, ComparatorResult } from '../utils/comparators';
|
||||||
import { SelectorInfo, Selectors } from './selectors';
|
import { SelectorInfo, Selectors } from './selectors';
|
||||||
import { CallMetadata, SdkObject } from './instrumentation';
|
import { CallMetadata, SdkObject } from './instrumentation';
|
||||||
import { Artifact } from './artifact';
|
import { Artifact } from './artifact';
|
||||||
import { ParsedSelector } from './common/selectorParser';
|
|
||||||
import { TimeoutOptions } from '../common/types';
|
import { TimeoutOptions } from '../common/types';
|
||||||
|
import { isInvalidSelectorError, ParsedSelector } from './common/selectorParser';
|
||||||
|
|
||||||
export interface PageDelegate {
|
export interface PageDelegate {
|
||||||
readonly rawMouse: input.RawMouse;
|
readonly rawMouse: input.RawMouse;
|
||||||
|
@ -94,6 +95,18 @@ type PageState = {
|
||||||
extraHTTPHeaders: types.HeadersArray | null;
|
extraHTTPHeaders: types.HeadersArray | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ExpectScreenshotOptions = {
|
||||||
|
timeout?: number,
|
||||||
|
expected?: Buffer,
|
||||||
|
isNot?: boolean,
|
||||||
|
locator?: {
|
||||||
|
frame: frames.Frame,
|
||||||
|
selector: string,
|
||||||
|
},
|
||||||
|
comparatorOptions?: ImageComparatorOptions,
|
||||||
|
screenshotOptions?: ScreenshotOptions,
|
||||||
|
};
|
||||||
|
|
||||||
export class Page extends SdkObject {
|
export class Page extends SdkObject {
|
||||||
static Events = {
|
static Events = {
|
||||||
Close: 'close',
|
Close: 'close',
|
||||||
|
@ -425,7 +438,65 @@ export class Page extends SdkObject {
|
||||||
route.continue();
|
route.continue();
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshot(metadata: CallMetadata, options: ScreenshotOptions & TimeoutOptions): Promise<Buffer> {
|
async expectScreenshot(metadata: CallMetadata, options: ExpectScreenshotOptions = {}): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[] }> {
|
||||||
|
const locator = options.locator;
|
||||||
|
const rafrafScreenshot = locator ? async (progress: Progress, timeout: number) => {
|
||||||
|
return await locator.frame.rafrafTimeoutScreenshotElementWithProgress(progress, locator.selector, timeout, options.screenshotOptions || {});
|
||||||
|
} : async (progress: Progress, timeout: number) => {
|
||||||
|
await this.mainFrame().rafrafTimeout(timeout);
|
||||||
|
return await this._screenshotter.screenshotPage(progress, options.screenshotOptions || {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const comparator = mimeTypeToComparator['image/png'];
|
||||||
|
const controller = new ProgressController(metadata, this);
|
||||||
|
const isGeneratingNewScreenshot = !options.expected;
|
||||||
|
if (isGeneratingNewScreenshot && options.isNot)
|
||||||
|
return { errorMessage: '"not" matcher requires expected result' };
|
||||||
|
let intermediateResult: {
|
||||||
|
actual?: Buffer,
|
||||||
|
previous?: Buffer,
|
||||||
|
errorMessage?: string,
|
||||||
|
diff?: Buffer,
|
||||||
|
} | undefined = undefined;
|
||||||
|
return controller.run(async progress => {
|
||||||
|
let actual: Buffer | undefined;
|
||||||
|
let previous: Buffer | undefined;
|
||||||
|
let screenshotTimeout = 0;
|
||||||
|
while (true) {
|
||||||
|
progress.throwIfAborted();
|
||||||
|
if (this.isClosed())
|
||||||
|
throw new Error('The page has closed');
|
||||||
|
let comparatorResult: ComparatorResult | undefined;
|
||||||
|
if (isGeneratingNewScreenshot) {
|
||||||
|
previous = actual;
|
||||||
|
actual = await rafrafScreenshot(progress, screenshotTimeout);
|
||||||
|
comparatorResult = actual && previous ? comparator(actual, previous, options.comparatorOptions) : undefined;
|
||||||
|
} else {
|
||||||
|
actual = await rafrafScreenshot(progress, screenshotTimeout);
|
||||||
|
comparatorResult = actual ? comparator(actual, options.expected!, options.comparatorOptions) : undefined;
|
||||||
|
}
|
||||||
|
screenshotTimeout = 150;
|
||||||
|
if (comparatorResult !== undefined && !!comparatorResult === !!options.isNot)
|
||||||
|
break;
|
||||||
|
if (comparatorResult)
|
||||||
|
intermediateResult = { errorMessage: comparatorResult.errorMessage, diff: comparatorResult.diff, actual, previous };
|
||||||
|
}
|
||||||
|
|
||||||
|
return isGeneratingNewScreenshot ? { actual } : {};
|
||||||
|
}, this._timeoutSettings.timeout(options)).catch(e => {
|
||||||
|
// Q: Why not throw upon isSessionClosedError(e) as in other places?
|
||||||
|
// A: We want user to receive a friendly diff between actual and expected/previous.
|
||||||
|
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
|
||||||
|
throw e;
|
||||||
|
return {
|
||||||
|
log: metadata.log,
|
||||||
|
...intermediateResult,
|
||||||
|
errorMessage: intermediateResult?.errorMessage ?? e.message,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async screenshot(metadata: CallMetadata, options: ScreenshotOptions & TimeoutOptions = {}): Promise<Buffer> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(
|
return controller.run(
|
||||||
progress => this._screenshotter.screenshotPage(progress, options),
|
progress => this._screenshotter.screenshotPage(progress, options),
|
||||||
|
|
|
@ -51,12 +51,13 @@ function compareImages(mimeType: string, actualBuffer: Buffer | string, expected
|
||||||
const expected = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpeg.decode(expectedBuffer);
|
const expected = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpeg.decode(expectedBuffer);
|
||||||
if (expected.width !== actual.width || expected.height !== actual.height) {
|
if (expected.width !== actual.width || expected.height !== actual.height) {
|
||||||
return {
|
return {
|
||||||
errorMessage: `Sizes differ; expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. `
|
errorMessage: `Expected an image ${expected.width}px by ${expected.height}px, received ${actual.width}px by ${actual.height}px. `
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const diff = new PNG({ width: expected.width, height: expected.height });
|
const diff = new PNG({ width: expected.width, height: expected.height });
|
||||||
const thresholdOptions = { threshold: 0.2, ...options };
|
const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, {
|
||||||
const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, thresholdOptions);
|
threshold: options.threshold ?? 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
const pixelCount1 = options.pixelCount;
|
const pixelCount1 = options.pixelCount;
|
||||||
const pixelCount2 = options.pixelRatio !== undefined ? expected.width * expected.height * options.pixelRatio : undefined;
|
const pixelCount2 = options.pixelRatio !== undefined ? expected.width * expected.height * options.pixelRatio : undefined;
|
||||||
|
|
|
@ -9373,50 +9373,7 @@ export interface Locator {
|
||||||
* screenshot. If the element is detached from DOM, the method throws an error.
|
* screenshot. If the element is detached from DOM, the method throws an error.
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
screenshot(options?: {
|
screenshot(options?: LocatorScreenshotOptions): Promise<Buffer>;
|
||||||
/**
|
|
||||||
* When true, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on
|
|
||||||
* their duration:
|
|
||||||
*/
|
|
||||||
disableAnimations?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with a pink box
|
|
||||||
* `#FF00FF` that completely covers its bounding box.
|
|
||||||
*/
|
|
||||||
mask?: Array<Locator>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
|
|
||||||
* Defaults to `false`.
|
|
||||||
*/
|
|
||||||
omitBackground?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative
|
|
||||||
* path, then it is resolved relative to the current working directory. If no path is provided, the image won't be saved to
|
|
||||||
* the disk.
|
|
||||||
*/
|
|
||||||
path?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The quality of the image, between 0-100. Not applicable to `png` images.
|
|
||||||
*/
|
|
||||||
quality?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by
|
|
||||||
* using the
|
|
||||||
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
|
|
||||||
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
|
|
||||||
*/
|
|
||||||
timeout?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify screenshot type, defaults to `png`.
|
|
||||||
*/
|
|
||||||
type?: "png"|"jpeg";
|
|
||||||
}): Promise<Buffer>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method waits for [actionability](https://playwright.dev/docs/actionability) checks, then tries to scroll element into view, unless it is
|
* This method waits for [actionability](https://playwright.dev/docs/actionability) checks, then tries to scroll element into view, unless it is
|
||||||
|
@ -15607,6 +15564,51 @@ export interface ConnectOptions {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LocatorScreenshotOptions {
|
||||||
|
/**
|
||||||
|
* When true, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on
|
||||||
|
* their duration:
|
||||||
|
*/
|
||||||
|
disableAnimations?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with a pink box
|
||||||
|
* `#FF00FF` that completely covers its bounding box.
|
||||||
|
*/
|
||||||
|
mask?: Array<Locator>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
|
||||||
|
* Defaults to `false`.
|
||||||
|
*/
|
||||||
|
omitBackground?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative
|
||||||
|
* path, then it is resolved relative to the current working directory. If no path is provided, the image won't be saved to
|
||||||
|
* the disk.
|
||||||
|
*/
|
||||||
|
path?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The quality of the image, between 0-100. Not applicable to `png` images.
|
||||||
|
*/
|
||||||
|
quality?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by
|
||||||
|
* using the
|
||||||
|
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
|
||||||
|
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify screenshot type, defaults to `png`.
|
||||||
|
*/
|
||||||
|
type?: "png"|"jpeg";
|
||||||
|
}
|
||||||
|
|
||||||
interface ElementHandleWaitForSelectorOptions {
|
interface ElementHandleWaitForSelectorOptions {
|
||||||
/**
|
/**
|
||||||
* Defaults to `'visible'`. Can be either:
|
* Defaults to `'visible'`. Can be either:
|
||||||
|
|
|
@ -43,7 +43,7 @@ import {
|
||||||
toHaveURL,
|
toHaveURL,
|
||||||
toHaveValue
|
toHaveValue
|
||||||
} from './matchers/matchers';
|
} from './matchers/matchers';
|
||||||
import { toMatchSnapshot } from './matchers/toMatchSnapshot';
|
import { toMatchSnapshot, toHaveScreenshot } from './matchers/toMatchSnapshot';
|
||||||
import type { Expect, TestError } from './types';
|
import type { Expect, TestError } from './types';
|
||||||
import matchers from 'expect/build/matchers';
|
import matchers from 'expect/build/matchers';
|
||||||
import { currentTestInfo } from './globals';
|
import { currentTestInfo } from './globals';
|
||||||
|
@ -132,6 +132,7 @@ const customMatchers = {
|
||||||
toHaveURL,
|
toHaveURL,
|
||||||
toHaveValue,
|
toHaveValue,
|
||||||
toMatchSnapshot,
|
toMatchSnapshot,
|
||||||
|
toHaveScreenshot,
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExpectMetaInfo = {
|
type ExpectMetaInfo = {
|
||||||
|
|
|
@ -14,10 +14,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Locator, Page } from 'playwright-core';
|
||||||
|
import type { Page as PageEx } from 'playwright-core/lib/client/page';
|
||||||
|
import type { Locator as LocatorEx } from 'playwright-core/lib/client/locator';
|
||||||
import type { Expect } from '../types';
|
import type { Expect } from '../types';
|
||||||
import { currentTestInfo } from '../globals';
|
import { currentTestInfo } from '../globals';
|
||||||
import { mimeTypeToComparator, ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils/comparators';
|
import { mimeTypeToComparator, ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils/comparators';
|
||||||
import { addSuffixToFilePath, serializeError, sanitizeForFilePath, trimLongString, callLogText } from '../util';
|
import type { PageScreenshotOptions } from 'playwright-core/types/types';
|
||||||
|
import { addSuffixToFilePath, serializeError, sanitizeForFilePath, trimLongString, callLogText, currentExpectTimeout } from '../util';
|
||||||
import { UpdateSnapshots } from '../types';
|
import { UpdateSnapshots } from '../types';
|
||||||
import colors from 'colors/safe';
|
import colors from 'colors/safe';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
@ -41,8 +45,10 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
|
||||||
readonly actualPath: string;
|
readonly actualPath: string;
|
||||||
readonly diffPath: string;
|
readonly diffPath: string;
|
||||||
readonly mimeType: string;
|
readonly mimeType: string;
|
||||||
|
readonly kind: 'Screenshot'|'Snapshot';
|
||||||
readonly updateSnapshots: UpdateSnapshots;
|
readonly updateSnapshots: UpdateSnapshots;
|
||||||
readonly comparatorOptions: T;
|
readonly comparatorOptions: ImageComparatorOptions;
|
||||||
|
readonly allOptions: T;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
testInfo: TestInfoImpl,
|
testInfo: TestInfoImpl,
|
||||||
|
@ -103,7 +109,13 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
|
||||||
this.diffPath = diffPath;
|
this.diffPath = diffPath;
|
||||||
this.snapshotPath = snapshotPath;
|
this.snapshotPath = snapshotPath;
|
||||||
this.updateSnapshots = updateSnapshots;
|
this.updateSnapshots = updateSnapshots;
|
||||||
this.comparatorOptions = options;
|
this.allOptions = options;
|
||||||
|
this.comparatorOptions = {
|
||||||
|
pixelCount: options.pixelCount,
|
||||||
|
pixelRatio: options.pixelRatio,
|
||||||
|
threshold: options.threshold,
|
||||||
|
};
|
||||||
|
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMissingNegated() {
|
handleMissingNegated() {
|
||||||
|
@ -123,7 +135,7 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
|
||||||
|
|
||||||
handleMatchingNegated() {
|
handleMatchingNegated() {
|
||||||
const message = [
|
const message = [
|
||||||
colors.red('Snapshot comparison failed:'),
|
colors.red(`${this.kind} comparison failed:`),
|
||||||
'',
|
'',
|
||||||
indent('Expected result should be different from the actual one.', ' '),
|
indent('Expected result should be different from the actual one.', ' '),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
@ -156,7 +168,7 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
|
||||||
diff: Buffer | string | undefined,
|
diff: Buffer | string | undefined,
|
||||||
diffError: string | undefined,
|
diffError: string | undefined,
|
||||||
log: string[] | undefined,
|
log: string[] | undefined,
|
||||||
title = `Snapshot comparison failed:`) {
|
title = `${this.kind} comparison failed:`) {
|
||||||
const output = [
|
const output = [
|
||||||
colors.red(title),
|
colors.red(title),
|
||||||
'',
|
'',
|
||||||
|
@ -232,6 +244,104 @@ export function toMatchSnapshot(
|
||||||
return helper.handleDifferent(received, expected, result.diff, result.errorMessage, undefined);
|
return helper.handleDifferent(received, expected, result.diff, result.errorMessage, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HaveScreenshotOptions = ImageComparatorOptions & Omit<PageScreenshotOptions, 'type' | 'quality' | 'path'>;
|
||||||
|
|
||||||
|
export async function toHaveScreenshot(
|
||||||
|
this: ReturnType<Expect['getState']>,
|
||||||
|
pageOrLocator: Page | Locator,
|
||||||
|
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & HaveScreenshotOptions = {},
|
||||||
|
optOptions: HaveScreenshotOptions = {}
|
||||||
|
): Promise<SyncExpectationResult> {
|
||||||
|
const testInfo = currentTestInfo();
|
||||||
|
if (!testInfo)
|
||||||
|
throw new Error(`toHaveScreenshot() must be called during the test`);
|
||||||
|
const helper = new SnapshotHelper(testInfo, 'png', nameOrOptions, optOptions);
|
||||||
|
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as LocatorEx];
|
||||||
|
const screenshotOptions = {
|
||||||
|
...helper.allOptions,
|
||||||
|
mask: (helper.allOptions.mask || []) as LocatorEx[],
|
||||||
|
name: undefined,
|
||||||
|
threshold: undefined,
|
||||||
|
pixelCount: undefined,
|
||||||
|
pixelRatio: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasSnapshot = fs.existsSync(helper.snapshotPath);
|
||||||
|
if (this.isNot) {
|
||||||
|
if (!hasSnapshot)
|
||||||
|
return helper.handleMissingNegated();
|
||||||
|
|
||||||
|
// Having `errorMessage` means we timed out while waiting
|
||||||
|
// for screenshots not to match, so screenshots
|
||||||
|
// are actually the same in the end.
|
||||||
|
const isDifferent = !(await page._expectScreenshot({
|
||||||
|
expected: await fs.promises.readFile(helper.snapshotPath),
|
||||||
|
isNot: true,
|
||||||
|
locator,
|
||||||
|
comparatorOptions: helper.comparatorOptions,
|
||||||
|
screenshotOptions,
|
||||||
|
timeout: currentExpectTimeout(helper.allOptions),
|
||||||
|
})).errorMessage;
|
||||||
|
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast path: there's no screenshot and we don't intend to update it.
|
||||||
|
if (helper.updateSnapshots === 'none' && !hasSnapshot)
|
||||||
|
return { pass: false, message: () => `${helper.snapshotPath} is missing in snapshots.` };
|
||||||
|
|
||||||
|
if (helper.updateSnapshots === 'all' || !hasSnapshot) {
|
||||||
|
// Regenerate a new screenshot by waiting until two screenshots are the same.
|
||||||
|
const timeout = currentExpectTimeout(helper.allOptions);
|
||||||
|
const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot({
|
||||||
|
expected: undefined,
|
||||||
|
isNot: false,
|
||||||
|
locator,
|
||||||
|
comparatorOptions: helper.comparatorOptions,
|
||||||
|
screenshotOptions,
|
||||||
|
timeout,
|
||||||
|
});
|
||||||
|
// We tried re-generating new snapshot but failed.
|
||||||
|
// This can be due to e.g. spinning animation, so we want to show it as a diff.
|
||||||
|
if (errorMessage) {
|
||||||
|
// TODO(aslushnikov): rename attachments to "actual" and "previous". They still should be somehow shown in HTML reporter.
|
||||||
|
const title = actual && previous ?
|
||||||
|
`Timeout ${timeout}ms exceeded while generating screenshot because ${locator ? 'element' : 'page'} kept changing:` :
|
||||||
|
`Timeout ${timeout}ms exceeded while generating screenshot:`;
|
||||||
|
return helper.handleDifferent(actual, previous, diff, undefined, log, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We successfully (re-)generated new screenshot.
|
||||||
|
if (!hasSnapshot)
|
||||||
|
return helper.handleMissing(actual!);
|
||||||
|
|
||||||
|
writeFileSync(helper.snapshotPath, actual!);
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
console.log(helper.snapshotPath + ' is re-generated, writing actual.');
|
||||||
|
return {
|
||||||
|
pass: true,
|
||||||
|
message: () => helper.snapshotPath + ' running with --update-snapshots, writing actual.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// General case:
|
||||||
|
// - snapshot exists
|
||||||
|
// - regular matcher (i.e. not a `.not`)
|
||||||
|
// - no flags to update screenshots
|
||||||
|
const expected = await fs.promises.readFile(helper.snapshotPath);
|
||||||
|
const { actual, diff, errorMessage, log } = await page._expectScreenshot({
|
||||||
|
expected,
|
||||||
|
isNot: false,
|
||||||
|
locator,
|
||||||
|
comparatorOptions: helper.comparatorOptions,
|
||||||
|
screenshotOptions,
|
||||||
|
timeout: currentExpectTimeout(helper.allOptions),
|
||||||
|
});
|
||||||
|
|
||||||
|
return errorMessage ?
|
||||||
|
helper.handleDifferent(actual, expected, diff, errorMessage, log) :
|
||||||
|
helper.handleMatching();
|
||||||
|
}
|
||||||
|
|
||||||
function writeFileSync(aPath: string, content: Buffer | string) {
|
function writeFileSync(aPath: string, content: Buffer | string) {
|
||||||
fs.mkdirSync(path.dirname(aPath), { recursive: true });
|
fs.mkdirSync(path.dirname(aPath), { recursive: true });
|
||||||
fs.writeFileSync(aPath, content);
|
fs.writeFileSync(aPath, content);
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as expect from 'expect';
|
import type * as expect from 'expect';
|
||||||
import type { Page, Locator, APIResponse } from 'playwright-core';
|
import type { Page, Locator, APIResponse, PageScreenshotOptions, LocatorScreenshotOptions } from 'playwright-core';
|
||||||
|
|
||||||
export declare type AsymmetricMatcher = Record<string, any>;
|
export declare type AsymmetricMatcher = Record<string, any>;
|
||||||
|
|
||||||
|
@ -46,6 +46,12 @@ export declare type Expect = {
|
||||||
stringMatching(expected: string | RegExp): AsymmetricMatcher;
|
stringMatching(expected: string | RegExp): AsymmetricMatcher;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ImageComparatorOptions = {
|
||||||
|
threshold?: number,
|
||||||
|
pixelCount?: number,
|
||||||
|
pixelRatio?: number,
|
||||||
|
};
|
||||||
|
|
||||||
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
||||||
|
|
||||||
type OverriddenExpectProperties =
|
type OverriddenExpectProperties =
|
||||||
|
@ -77,18 +83,13 @@ declare global {
|
||||||
/**
|
/**
|
||||||
* Match snapshot
|
* Match snapshot
|
||||||
*/
|
*/
|
||||||
toMatchSnapshot(options?: {
|
toMatchSnapshot(options?: ImageComparatorOptions & {
|
||||||
name?: string | string[],
|
name?: string | string[],
|
||||||
threshold?: number,
|
|
||||||
pixelCount?: number,
|
|
||||||
pixelRatio?: number,
|
|
||||||
}): R;
|
}): R;
|
||||||
/**
|
/**
|
||||||
* Match snapshot
|
* Match snapshot
|
||||||
*/
|
*/
|
||||||
toMatchSnapshot(name: string | string[], options?: {
|
toMatchSnapshot(name: string | string[], options?: ImageComparatorOptions): R;
|
||||||
threshold?: number
|
|
||||||
}): R;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,6 +179,18 @@ interface LocatorMatchers {
|
||||||
* Asserts given DOM node visible on the screen.
|
* Asserts given DOM node visible on the screen.
|
||||||
*/
|
*/
|
||||||
toBeVisible(options?: { timeout?: number }): Promise<Locator>;
|
toBeVisible(options?: { timeout?: number }): Promise<Locator>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts element's screenshot is matching to the snapshot.
|
||||||
|
*/
|
||||||
|
toHaveScreenshot(options?: Omit<LocatorScreenshotOptions, 'path' | 'type' | 'quality'> & ImageComparatorOptions & {
|
||||||
|
name?: string | string[],
|
||||||
|
}): Promise<Locator>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts element's screenshot is matching to the snapshot.
|
||||||
|
*/
|
||||||
|
toHaveScreenshot(name: string | string[], options?: Omit<LocatorScreenshotOptions, 'path' | 'type' | 'quality'> & ImageComparatorOptions): Promise<Locator>;
|
||||||
}
|
}
|
||||||
interface PageMatchers {
|
interface PageMatchers {
|
||||||
/**
|
/**
|
||||||
|
@ -189,6 +202,18 @@ interface PageMatchers {
|
||||||
* Asserts page's URL.
|
* Asserts page's URL.
|
||||||
*/
|
*/
|
||||||
toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<Page>;
|
toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<Page>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts page screenshot is matching to the snapshot.
|
||||||
|
*/
|
||||||
|
toHaveScreenshot(options?: Omit<PageScreenshotOptions, 'path' | 'quality' | 'type'> & ImageComparatorOptions & {
|
||||||
|
name?: string | string[],
|
||||||
|
}): Promise<Page>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts page screenshot is matching to the snapshot.
|
||||||
|
*/
|
||||||
|
toHaveScreenshot(name: string | string[], options?: Omit<PageScreenshotOptions, 'path' | 'quality' | 'type'> & ImageComparatorOptions): Promise<Page>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APIResponseMatchers {
|
interface APIResponseMatchers {
|
||||||
|
|
|
@ -74,8 +74,8 @@ test('should generate default name', async ({ runInlineTest }, testInfo) => {
|
||||||
|
|
||||||
test('should compile with different option combinations', async ({ runTSC }) => {
|
test('should compile with different option combinations', async ({ runTSC }) => {
|
||||||
const result = await runTSC({
|
const result = await runTSC({
|
||||||
'a.spec.js': `
|
'a.spec.ts': `
|
||||||
const { test, expect } = pwt;
|
const { test } = pwt;
|
||||||
test('is a test', async ({ page }) => {
|
test('is a test', async ({ page }) => {
|
||||||
expect('foo').toMatchSnapshot();
|
expect('foo').toMatchSnapshot();
|
||||||
expect('foo').toMatchSnapshot({ threshold: 0.2 });
|
expect('foo').toMatchSnapshot({ threshold: 0.2 });
|
||||||
|
@ -610,7 +610,7 @@ test('should compare different PNG images', async ({ runInlineTest }, testInfo)
|
||||||
|
|
||||||
const outputText = stripAnsi(result.output);
|
const outputText = stripAnsi(result.output);
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(outputText).toContain('Snapshot comparison failed:');
|
expect(outputText).toContain('Screenshot comparison failed:');
|
||||||
const expectedSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-expected.png');
|
const expectedSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-expected.png');
|
||||||
const actualSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-actual.png');
|
const actualSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-actual.png');
|
||||||
const diffSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-diff.png');
|
const diffSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-diff.png');
|
||||||
|
@ -915,7 +915,7 @@ test('should attach expected/actual and no diff', async ({ runInlineTest }, test
|
||||||
});
|
});
|
||||||
|
|
||||||
const outputText = stripAnsi(result.output);
|
const outputText = stripAnsi(result.output);
|
||||||
expect(outputText).toContain('Sizes differ; expected image 2px X 2px, but got 1px X 1px.');
|
expect(outputText).toContain('Expected an image 2px by 2px, received 1px by 1px.');
|
||||||
const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0];
|
const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0];
|
||||||
for (const attachment of attachments)
|
for (const attachment of attachments)
|
||||||
attachment.path = attachment.path.replace(/\\/g, '/').replace(/.*test-results\//, '');
|
attachment.path = attachment.path.replace(/\\/g, '/').replace(/.*test-results\//, '');
|
||||||
|
|
|
@ -267,14 +267,14 @@ export function countTimes(s: string, sub: string): number {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createImage(width: number, height: number, r: number = 0, g: number = 0, b: number = 0): Buffer {
|
export function createImage(width: number, height: number, r: number = 0, g: number = 0, b: number = 0, a: number = 255): Buffer {
|
||||||
const image = new PNG({ width, height });
|
const image = new PNG({ width, height });
|
||||||
// Make both images red.
|
// Make both images red.
|
||||||
for (let i = 0; i < width * height; ++i) {
|
for (let i = 0; i < width * height; ++i) {
|
||||||
image.data[i * 4 + 0] = r;
|
image.data[i * 4 + 0] = r;
|
||||||
image.data[i * 4 + 1] = g;
|
image.data[i * 4 + 1] = g;
|
||||||
image.data[i * 4 + 2] = b;
|
image.data[i * 4 + 2] = b;
|
||||||
image.data[i * 4 + 3] = 255;
|
image.data[i * 4 + 3] = a;
|
||||||
}
|
}
|
||||||
return PNG.sync.write(image);
|
return PNG.sync.write(image);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,645 @@
|
||||||
|
/**
|
||||||
|
* Copyright 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 { mimeTypeToComparator } from 'playwright-core/lib/utils/comparators';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { PNG } from 'pngjs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { pathToFileURL } from 'url';
|
||||||
|
import { test, expect, stripAnsi, createImage, paintBlackPixels } from './playwright-test-fixtures';
|
||||||
|
|
||||||
|
const pngComparator = mimeTypeToComparator['image/png'];
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
|
const IMG_WIDTH = 1280;
|
||||||
|
const IMG_HEIGHT = 720;
|
||||||
|
const whiteImage = createImage(IMG_WIDTH, IMG_HEIGHT, 255, 255, 255);
|
||||||
|
const redImage = createImage(IMG_WIDTH, IMG_HEIGHT, 255, 0, 0);
|
||||||
|
const greenImage = createImage(IMG_WIDTH, IMG_HEIGHT, 0, 255, 0);
|
||||||
|
const blueImage = createImage(IMG_WIDTH, IMG_HEIGHT, 0, 0, 255);
|
||||||
|
|
||||||
|
const files = {
|
||||||
|
'helper.ts': `
|
||||||
|
export const test = pwt.test.extend({
|
||||||
|
auto: [ async ({}, run, testInfo) => {
|
||||||
|
testInfo.snapshotSuffix = '';
|
||||||
|
await run();
|
||||||
|
}, { auto: true } ]
|
||||||
|
});
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should fail to screenshot a page with infinite animation', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await page.goto('${infiniteAnimationURL}');
|
||||||
|
await expect(page).toHaveScreenshot({ timeout: 2000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(stripAnsi(result.output)).toContain(`Timeout 2000ms exceeded while generating screenshot because page kept changing`);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(true);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-diff.png'))).toBe(true);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not fail when racing with navigation', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': createImage(10, 10, 255, 0, 0),
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await Promise.all([
|
||||||
|
page.goto('${infiniteAnimationURL}'),
|
||||||
|
expect(page).toHaveScreenshot({
|
||||||
|
name: 'snapshot.png',
|
||||||
|
disableAnimations: true,
|
||||||
|
clip: { x: 0, y: 0, width: 10, height: 10 },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully screenshot a page with infinite animation with disableAnimation: true', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await page.goto('${infiniteAnimationURL}');
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
disableAnimations: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': true });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support clip option for page', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': createImage(50, 50, 255, 255, 255),
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
name: 'snapshot.png',
|
||||||
|
clip: { x: 0, y: 0, width: 50, height: 50, },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support omitBackground option for locator', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.body.style.setProperty('width', '100px');
|
||||||
|
document.body.style.setProperty('height', '100px');
|
||||||
|
});
|
||||||
|
await expect(page.locator('body')).toHaveScreenshot({
|
||||||
|
name: 'snapshot.png',
|
||||||
|
omitBackground: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': true });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const snapshotPath = testInfo.outputPath('a.spec.js-snapshots', 'snapshot.png');
|
||||||
|
expect(fs.existsSync(snapshotPath)).toBe(true);
|
||||||
|
const png = PNG.sync.read(fs.readFileSync(snapshotPath));
|
||||||
|
expect.soft(png.width, 'image width must be 100').toBe(100);
|
||||||
|
expect.soft(png.height, 'image height must be 100').toBe(100);
|
||||||
|
expect.soft(png.data[0], 'image R must be 0').toBe(0);
|
||||||
|
expect.soft(png.data[1], 'image G must be 0').toBe(0);
|
||||||
|
expect.soft(png.data[2], 'image B must be 0').toBe(0);
|
||||||
|
expect.soft(png.data[3], 'image A must be 0').toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail to screenshot an element with infinite animation', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await page.goto('${infiniteAnimationURL}');
|
||||||
|
await expect(page.locator('body')).toHaveScreenshot({ timeout: 2000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(stripAnsi(result.output)).toContain(`Timeout 2000ms exceeded while generating screenshot because element kept changing`);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(true);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-diff.png'))).toBe(true);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail to screenshot an element that keeps moving', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await page.goto('${infiniteAnimationURL}');
|
||||||
|
await expect(page.locator('div')).toHaveScreenshot({ timeout: 2000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(stripAnsi(result.output)).toContain(`Timeout 2000ms exceeded`);
|
||||||
|
expect(stripAnsi(result.output)).toContain(`element is not stable - waiting`);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(false);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(false);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-diff.png'))).toBe(false);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate default name', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot();
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true);
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should compile with different option combinations', async ({ runTSC }) => {
|
||||||
|
const result = await runTSC({
|
||||||
|
'a.spec.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot();
|
||||||
|
await expect(page.locator('body')).toHaveScreenshot({ threshold: 0.2 });
|
||||||
|
await expect(page).toHaveScreenshot({ pixelRatio: 0.2 });
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
threshold: 0.2,
|
||||||
|
pixelCount: 10,
|
||||||
|
pixelRatio: 0.2,
|
||||||
|
disableAnimations: true,
|
||||||
|
omitBackground: true,
|
||||||
|
timeout: 1000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail when screenshot is different size', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': createImage(22, 33),
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain('Expected an image 22px by 33px, received 1280px by 720px.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail when screenshot is different pixels', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': blueImage,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain('Timeout 2000ms exceeded');
|
||||||
|
expect(result.output).toContain('Screenshot comparison failed');
|
||||||
|
expect(result.output).toContain('Expected:');
|
||||||
|
expect(result.output).toContain('Received:');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('doesn\'t create comparison artifacts in an output folder for passed negated snapshot matcher', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': blueImage,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).not.toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const outputText = stripAnsi(result.output);
|
||||||
|
const expectedSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-expected.png');
|
||||||
|
const actualSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-actual.png');
|
||||||
|
expect(outputText).not.toContain(`Expected: ${expectedSnapshotArtifactPath}`);
|
||||||
|
expect(outputText).not.toContain(`Received: ${actualSnapshotArtifactPath}`);
|
||||||
|
expect(fs.existsSync(expectedSnapshotArtifactPath)).toBe(false);
|
||||||
|
expect(fs.existsSync(actualSnapshotArtifactPath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail on same snapshots with negate matcher', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': whiteImage,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).not.toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain('Screenshot comparison failed:');
|
||||||
|
expect(result.output).toContain('Expected result should be different from the actual one.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should write missing expectations locally twice and continue', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png');
|
||||||
|
await expect(page).toHaveScreenshot('snapshot2.png');
|
||||||
|
console.log('Here we are!');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
|
||||||
|
const snapshot1OutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||||
|
expect(result.output).toContain(`Error: ${snapshot1OutputPath} is missing in snapshots, writing actual`);
|
||||||
|
expect(pngComparator(fs.readFileSync(snapshot1OutputPath), whiteImage)).toBe(null);
|
||||||
|
|
||||||
|
const snapshot2OutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot2.png');
|
||||||
|
expect(result.output).toContain(`Error: ${snapshot2OutputPath} is missing in snapshots, writing actual`);
|
||||||
|
expect(pngComparator(fs.readFileSync(snapshot2OutputPath), whiteImage)).toBe(null);
|
||||||
|
|
||||||
|
expect(result.output).toContain('Here we are!');
|
||||||
|
|
||||||
|
const stackLines = stripAnsi(result.output).split('\n').filter(line => line.includes(' at ')).filter(line => !line.includes(testInfo.outputPath()));
|
||||||
|
expect(result.output).toContain('a.spec.js:8');
|
||||||
|
expect(stackLines.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shouldn\'t write missing expectations locally for negated matcher', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).not.toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||||
|
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, matchers using ".not" won\'t write them automatically.`);
|
||||||
|
expect(fs.existsSync(snapshotOutputPath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update snapshot with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': blueImage,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': true });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||||
|
expect(result.output).toContain(`${snapshotOutputPath} is re-generated, writing actual.`);
|
||||||
|
expect(pngComparator(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shouldn\'t update snapshot with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const EXPECTED_SNAPSHOT = blueImage;
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).not.toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': true });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||||
|
expect(fs.readFileSync(snapshotOutputPath).equals(EXPECTED_SNAPSHOT)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should silently write missing expectations locally with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': true });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||||
|
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, writing actual`);
|
||||||
|
const data = fs.readFileSync(snapshotOutputPath);
|
||||||
|
expect(pngComparator(data, whiteImage)).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not write missing expectations locally with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).not.toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': true });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||||
|
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, matchers using ".not" won\'t write them automatically.`);
|
||||||
|
expect(fs.existsSync(snapshotOutputPath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match multiple snapshots', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/red.png': redImage,
|
||||||
|
'a.spec.js-snapshots/green.png': greenImage,
|
||||||
|
'a.spec.js-snapshots/blue.png': blueImage,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await Promise.all([
|
||||||
|
page.evaluate(() => document.documentElement.style.setProperty('background', '#f00')),
|
||||||
|
expect(page).toHaveScreenshot('red.png'),
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
page.evaluate(() => document.documentElement.style.setProperty('background', '#0f0')),
|
||||||
|
expect(page).toHaveScreenshot('green.png'),
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
page.evaluate(() => document.documentElement.style.setProperty('background', '#00f')),
|
||||||
|
expect(page).toHaveScreenshot('blue.png'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use provided name', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/provided.png': whiteImage,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('provided.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use provided name via options', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/provided.png': whiteImage,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot({ name: 'provided.png' });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect pixelCount option', async ({ runInlineTest }) => {
|
||||||
|
const BAD_PIXELS = 120;
|
||||||
|
const EXPECTED_SNAPSHOT = paintBlackPixels(whiteImage, BAD_PIXELS);
|
||||||
|
|
||||||
|
expect((await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
})).exitCode, 'make sure default comparison fails').toBe(1);
|
||||||
|
|
||||||
|
expect((await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png', {
|
||||||
|
pixelCount: ${BAD_PIXELS}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
})).exitCode, 'make sure pixelCount option is respected').toBe(0);
|
||||||
|
|
||||||
|
expect((await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { projects: [
|
||||||
|
{ expect: { toMatchSnapshot: { pixelCount: ${BAD_PIXELS} } } },
|
||||||
|
]};
|
||||||
|
`,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
})).exitCode, 'make sure pixelCount option in project config is respected').toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect pixelRatio option', async ({ runInlineTest }) => {
|
||||||
|
const BAD_RATIO = 0.25;
|
||||||
|
const BAD_PIXELS = IMG_WIDTH * IMG_HEIGHT * BAD_RATIO;
|
||||||
|
const EXPECTED_SNAPSHOT = paintBlackPixels(whiteImage, BAD_PIXELS);
|
||||||
|
|
||||||
|
expect((await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
})).exitCode, 'make sure default comparison fails').toBe(1);
|
||||||
|
|
||||||
|
expect((await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png', {
|
||||||
|
pixelRatio: ${BAD_RATIO}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
})).exitCode, 'make sure pixelRatio option is respected').toBe(0);
|
||||||
|
|
||||||
|
expect((await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { projects: [
|
||||||
|
{ expect: { toMatchSnapshot: { pixelRatio: ${BAD_RATIO} } } },
|
||||||
|
]};
|
||||||
|
`,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
})).exitCode, 'make sure pixelCount option in project config is respected').toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should attach expected/actual and no diff when sizes are different', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.png': createImage(2, 2),
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test.afterEach(async ({}, testInfo) => {
|
||||||
|
console.log('## ' + JSON.stringify(testInfo.attachments));
|
||||||
|
});
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
const outputText = stripAnsi(result.output);
|
||||||
|
expect(outputText).toContain('Expected an image 2px by 2px, received 1280px by 720px.');
|
||||||
|
const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0];
|
||||||
|
for (const attachment of attachments)
|
||||||
|
attachment.path = attachment.path.replace(/\\/g, '/').replace(/.*test-results\//, '');
|
||||||
|
expect(attachments).toEqual([
|
||||||
|
{
|
||||||
|
name: 'expected',
|
||||||
|
contentType: 'image/png',
|
||||||
|
path: 'a-is-a-test/snapshot-expected.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actual',
|
||||||
|
contentType: 'image/png',
|
||||||
|
path: 'a-is-a-test/snapshot-actual.png'
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail with missing expectations and retries', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { retries: 1 };
|
||||||
|
`,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||||
|
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, writing actual`);
|
||||||
|
const data = fs.readFileSync(snapshotOutputPath);
|
||||||
|
expect(pngComparator(data, whiteImage)).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update expectations with retries', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { retries: 1 };
|
||||||
|
`,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = require('./helper');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': true });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||||
|
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, writing actual`);
|
||||||
|
const data = fs.readFileSync(snapshotOutputPath);
|
||||||
|
expect(pngComparator(data, whiteImage)).toBe(null);
|
||||||
|
});
|
||||||
|
|
|
@ -7,5 +7,6 @@
|
||||||
"BrowserNewContextOptionsViewport": "ViewportSize",
|
"BrowserNewContextOptionsViewport": "ViewportSize",
|
||||||
"BrowserNewContextOptionsGeolocation": "Geolocation",
|
"BrowserNewContextOptionsGeolocation": "Geolocation",
|
||||||
"BrowserNewContextOptionsHttpCredentials": "HTTPCredentials",
|
"BrowserNewContextOptionsHttpCredentials": "HTTPCredentials",
|
||||||
"PageScreenshotOptions": "PageScreenshotOptions"
|
"PageScreenshotOptions": "PageScreenshotOptions",
|
||||||
|
"LocatorScreenshotOptions": "LocatorScreenshotOptions"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue