diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 3a91e540a8a25..5800e5bf315e4 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -2008,6 +2008,9 @@ Snapshot name. Must have a `.png` or `.webp` extension, the screenshot is captur ### option: LocatorAssertions.toHaveScreenshot#1.timeout = %%-js-assertions-timeout-%% * since: v1.23 +### option: LocatorAssertions.toHaveScreenshot#1.signal = %%-js-assertions-signal-%% +* since: v1.62 + ### option: LocatorAssertions.toHaveScreenshot#1.animations = %%-screenshot-option-animations-default-disabled-%% * since: v1.23 @@ -2059,6 +2062,9 @@ Note that screenshot assertions only work with Playwright test runner. ### option: LocatorAssertions.toHaveScreenshot#2.timeout = %%-js-assertions-timeout-%% * since: v1.23 +### option: LocatorAssertions.toHaveScreenshot#2.signal = %%-js-assertions-signal-%% +* since: v1.62 + ### option: LocatorAssertions.toHaveScreenshot#2.animations = %%-screenshot-option-animations-default-disabled-%% * since: v1.23 diff --git a/docs/src/api/class-pageassertions.md b/docs/src/api/class-pageassertions.md index 5747aa609b597..22b1b95bfe52a 100644 --- a/docs/src/api/class-pageassertions.md +++ b/docs/src/api/class-pageassertions.md @@ -258,6 +258,9 @@ Snapshot name. Must have a `.png` or `.webp` extension, the screenshot is captur ### option: PageAssertions.toHaveScreenshot#1.timeout = %%-js-assertions-timeout-%% * since: v1.23 +### option: PageAssertions.toHaveScreenshot#1.signal = %%-js-assertions-signal-%% +* since: v1.62 + ### option: PageAssertions.toHaveScreenshot#1.animations = %%-screenshot-option-animations-default-disabled-%% * since: v1.23 @@ -314,6 +317,9 @@ Note that screenshot assertions only work with Playwright test runner. ### option: PageAssertions.toHaveScreenshot#2.timeout = %%-js-assertions-timeout-%% * since: v1.23 +### option: PageAssertions.toHaveScreenshot#2.signal = %%-js-assertions-signal-%% +* since: v1.62 + ### option: PageAssertions.toHaveScreenshot#2.animations = %%-screenshot-option-animations-default-disabled-%% * since: v1.23 diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 1fedf84e23ad9..8da92cb10b86f 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -19,6 +19,7 @@ import fs from 'fs'; import * as inspector from 'inspector'; import path from 'path'; +import { assertionAbortedMessage } from '@isomorphic/abortSignal'; import { assert } from '@isomorphic/assert'; import { headersObjectToArray } from '@isomorphic/headers'; import { trimStringWithEllipsis } from '@isomorphic/stringUtils'; @@ -32,7 +33,7 @@ import { Coverage } from './coverage'; import { DisposableObject, DisposableStub } from './disposable'; import { Download } from './download'; import { ElementHandle, determineScreenshotType } from './elementHandle'; -import { PlaywrightError, TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors'; +import { AbortError, PlaywrightError, TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors'; import { Events } from './events'; import { FileChooser } from './fileChooser'; import { Frame, verifyLoadState } from './frame'; @@ -77,6 +78,7 @@ export type ExpectScreenshotOptions = Omit implements api.Page } async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[], timedOut?: boolean}> { - const { timeout, ...optionsWithoutTimeout } = options; + const { timeout, signal, ...optionsWithoutTimeout } = options; const mask = options?.mask ? options?.mask.map(locator => ({ frame: (locator as Locator)._frame._channel, selector: (locator as Locator)._selector, @@ -637,9 +639,11 @@ export class Page extends ChannelOwner implements api.Page isNot: !!options.isNot, locator, mask, - }, { signal: undefined, timeout }); + }, { timeout, signal }); return { actual: result.actual }; } catch (e) { + if (e instanceof AbortError) + return { errorMessage: 'Error: ' + assertionAbortedMessage(e.cause) }; if (!(e instanceof PlaywrightError)) throw e; const details = e.details as channels.PageExpectScreenshotErrorDetails; diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts index 2b7a088c95c8d..2b79dec0674c4 100644 --- a/packages/playwright/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchSnapshot.ts @@ -55,6 +55,7 @@ type ToHaveScreenshotOptions = ToHaveScreenshotConfigOptions & { mask?: Array; maskColor?: string; omitBackground?: boolean; + signal?: AbortSignal; }; // Keep in sync with above (begin). @@ -64,6 +65,7 @@ const NonConfigProperties: (keyof ToHaveScreenshotOptions)[] = [ 'mask', 'maskColor', 'omitBackground', + 'signal', ]; // Keep in sync with above (end). @@ -362,6 +364,7 @@ export async function toHaveScreenshot( style, isNot: !!this.isNot, timeout, + signal: helper.options.signal, type: screenshotType, comparator: helper.options.comparator, maxDiffPixels: helper.options.maxDiffPixels, diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index d30c7f240de59..093da901d0804 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -9618,6 +9618,13 @@ interface LocatorAssertions { */ scale?: "css"|"device"; + /** + * An optional [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that can cancel the + * assertion. Aborting the signal fails the assertion like a timeout: if the signal is aborted while the assertion is + * retrying, or is already aborted before the assertion starts, the assertion fails without retrying further. + */ + signal?: AbortSignal; + /** * File name containing the stylesheet to apply while making the screenshot. This is where you can hide dynamic * elements, make elements invisible or change their properties to help you creating repeatable screenshots. This @@ -9714,6 +9721,13 @@ interface LocatorAssertions { */ scale?: "css"|"device"; + /** + * An optional [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that can cancel the + * assertion. Aborting the signal fails the assertion like a timeout: if the signal is aborted while the assertion is + * retrying, or is already aborted before the assertion starts, the assertion fails without retrying further. + */ + signal?: AbortSignal; + /** * File name containing the stylesheet to apply while making the screenshot. This is where you can hide dynamic * elements, make elements invisible or change their properties to help you creating repeatable screenshots. This @@ -10605,6 +10619,13 @@ export interface PageAssertionsToHaveScreenshotOptions { */ scale?: "css"|"device"; + /** + * An optional [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that can cancel the + * assertion. Aborting the signal fails the assertion like a timeout: if the signal is aborted while the assertion is + * retrying, or is already aborted before the assertion starts, the assertion fails without retrying further. + */ + signal?: AbortSignal; + /** * File name containing the stylesheet to apply while making the screenshot. This is where you can hide dynamic * elements, make elements invisible or change their properties to help you creating repeatable screenshots. This diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 79f33f51825cd..22551fa95d1c8 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -60,6 +60,43 @@ test('should fail to screenshot a page with infinite animation', async ({ runInl expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(false); }); +test('should fail like a timeout when aborted', async ({ runInlineTest }) => { + const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html')); + const result = await runInlineTest({ + ...playwrightConfig({}), + 'a.spec.js': ` + const { test, expect } = require('@playwright/test'); + test('is a test', async ({ page }) => { + await page.goto('${infiniteAnimationURL}'); + const controller = new AbortController(); + const promise = expect(page).toHaveScreenshot({ animations: 'allow', timeout: 5000, signal: controller.signal }); + await page.waitForTimeout(500); + controller.abort(new Error('stop it')); + await promise; + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`operation was aborted: stop it`); + expect(result.output).not.toContain(`Timeout 5000ms exceeded`); +}); + +test('should fail when already aborted', async ({ runInlineTest }) => { + const result = await runInlineTest({ + ...playwrightConfig({}), + 'a.spec.js': ` + const { test, expect } = require('@playwright/test'); + test('is a test', async ({ page }) => { + const controller = new AbortController(); + controller.abort(new Error('already aborted')); + await expect(page).toHaveScreenshot({ signal: controller.signal }); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`Error: The assertion was aborted: already aborted`); +}); + test('should disable animations by default', async ({ runInlineTest }, testInfo) => { const cssTransitionURL = pathToFileURL(path.join(__dirname, '../assets/css-transition.html')); const result = await runInlineTest({