diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index 91ce23c8b2..59a0a3ba38 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -40,6 +40,10 @@ export default config; - `threshold` <[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. - `maxDiffPixels` <[int]> an acceptable amount of pixels that could be different, unset by default. - `maxDiffPixelRatio` <[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default. + - `animations` <[ScreenshotAnimations]<"allow"|"disable">> See [`option: animations`] in [`method: Page.screenshot`]. Defaults to `"disable"`. + - `fonts` <[ScreenshotFonts]<"ready"|"nowait">> See [`option: fonts`] in [`method: Page.screenshot`]. Defaults to `"ready"`. + - `size` <[ScreenshotSize]<"css"|"device">> See [`option: size`] in [`method: Page.screenshot`]. Defaults to `"css"`. + - `toMatchSnapshot` <[Object]> - `threshold` <[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. - `maxDiffPixels` <[int]> an acceptable amount of pixels that could be different, unset by default. diff --git a/docs/src/test-api/class-testproject.md b/docs/src/test-api/class-testproject.md index 8a12422c46..ab0e7ded8d 100644 --- a/docs/src/test-api/class-testproject.md +++ b/docs/src/test-api/class-testproject.md @@ -111,6 +111,9 @@ export default config; - `threshold` <[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. - `maxDiffPixels` <[int]> an acceptable amount of pixels that could be different, unset by default. - `maxDiffPixelRatio` <[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default. + - `animations` <[ScreenshotAnimations]<"allow"|"disable">> See [`option: animations`] in [`method: Page.screenshot`]. Defaults to `"disable"`. + - `fonts` <[ScreenshotFonts]<"ready"|"nowait">> See [`option: fonts`] in [`method: Page.screenshot`]. Defaults to `"ready"`. + - `size` <[ScreenshotSize]<"css"|"device">> See [`option: size`] in [`method: Page.screenshot`]. Defaults to `"css"`. - `toMatchSnapshot` <[Object]> - `threshold` <[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. - `maxDiffPixels` <[int]> an acceptable amount of pixels that could be different, unset by default. diff --git a/packages/playwright-test/src/matchers/toMatchSnapshot.ts b/packages/playwright-test/src/matchers/toMatchSnapshot.ts index cf02f9aeac..398f66b880 100644 --- a/packages/playwright-test/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright-test/src/matchers/toMatchSnapshot.ts @@ -285,12 +285,20 @@ export async function toHaveScreenshot( const testInfo = currentTestInfo(); if (!testInfo) throw new Error(`toHaveScreenshot() must be called during the test`); + const config = testInfo.project.expect?.toHaveScreenshot; const helper = new SnapshotHelper( testInfo, testInfo._screenshotPath.bind(testInfo), 'png', - testInfo.project.expect?.toHaveScreenshot || {}, + { + maxDiffPixels: config?.maxDiffPixels, + maxDiffPixelRatio: config?.maxDiffPixelRatio, + threshold: config?.threshold, + }, nameOrOptions, optOptions); const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as LocatorEx]; const screenshotOptions = { + animations: config?.animations ?? 'disabled', + fonts: config?.fonts ?? 'ready', + size: config?.size ?? 'css', ...helper.allOptions, mask: (helper.allOptions.mask || []) as LocatorEx[], name: undefined, diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index e0159672fe..01b5c0dd86 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -55,6 +55,27 @@ type ExpectSettings = { * An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default. */ maxDiffPixelRatio?: number, + /** + * When set to `"disabled"`, 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. + * + * Defaults to `"disabled"` that leaves animations untouched. + */ + animations?: 'allow'|'disabled', + /** + * When set to `"ready"`, screenshot will wait for + * [`document.fonts.ready`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) promise to resolve in all + * frames. Defaults to `"ready"`. + */ + fonts?: 'ready'|'nowait', + /** + * When set to `"css"`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will + * keep screenshots small. Using `"device"` option will produce a single pixel per each device pixel, so screenhots of + * high-dpi devices will be twice as large or even larger. Defaults to `"css"`. + */ + size?: 'css'|'device', } toMatchSnapshot?: { /** An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax). Defaults to `0.2`. diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 1e8477325f..c91a31d9ac 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -35,6 +35,13 @@ const blueImage = createImage(IMG_WIDTH, IMG_HEIGHT, 0, 0, 255); 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({ + ...playwrightConfig({ + expect: { + toHaveScreenshot: { + animations: 'allow', + }, + }, + }), 'a.spec.js': ` pwt.test('is a test', async ({ page }) => { await page.goto('${infiniteAnimationURL}'); @@ -50,6 +57,62 @@ 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 disable animations by default', async ({ runInlineTest }, testInfo) => { + const cssTransitionURL = pathToFileURL(path.join(__dirname, '../assets/css-transition.html')); + const result = await runInlineTest({ + 'a.spec.js': ` + pwt.test('is a test', async ({ page }) => { + await page.goto('${cssTransitionURL}'); + await expect(page).toHaveScreenshot({ timeout: 2000 }); + }); + ` + }, { 'update-snapshots': true }); + expect(result.exitCode).toBe(0); +}); + +test('should have size as css by default', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + ...playwrightConfig({ screenshotsDir: '__screenshots__' }), + 'a.spec.js': ` + pwt.test('is a test', async ({ browser }) => { + const context = await browser.newContext({ + viewport: { width: ${IMG_WIDTH}, height: ${IMG_HEIGHT} }, + deviceScaleFactor: 2, + }); + const page = await context.newPage(); + await expect(page).toHaveScreenshot('snapshot.png'); + await context.close(); + }); + ` + }, { 'update-snapshots': true }); + expect(result.exitCode).toBe(0); + + const snapshotOutputPath = testInfo.outputPath('__screenshots__', 'a.spec.js', 'snapshot.png'); + expect(pngComparator(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null); +}); + +test('should ignore non-documented options in toHaveScreenshot config', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + ...playwrightConfig({ + screenshotsDir: '__screenshots__', + expect: { + toHaveScreenshot: { + clip: { x: 0, y: 0, width: 10, height: 10 }, + }, + }, + }), + 'a.spec.js': ` + pwt.test('is a test', async ({ page }) => { + await expect(page).toHaveScreenshot('snapshot.png'); + }); + ` + }, { 'update-snapshots': true }); + expect(result.exitCode).toBe(0); + + const snapshotOutputPath = testInfo.outputPath('__screenshots__', 'a.spec.js', 'snapshot.png'); + expect(pngComparator(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null); +}); + test('screenshotPath should include platform and project name by default', async ({ runInlineTest }, testInfo) => { const PROJECT_NAME = 'woof-woof'; const result = await runInlineTest({ @@ -203,7 +266,16 @@ test('should support omitBackground option for locator', async ({ runInlineTest 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({ - ...playwrightConfig({ screenshotsDir: '__screenshots__' }), + ...playwrightConfig({ + screenshotsDir: '__screenshots__', + projects: [{ + expect: { + toHaveScreenshot: { + animations: 'allow', + }, + }, + }], + }), 'a.spec.js': ` pwt.test('is a test', async ({ page }) => { await page.goto('${infiniteAnimationURL}'); @@ -222,7 +294,14 @@ test('should fail to screenshot an element with infinite animation', async ({ ru 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({ - ...playwrightConfig({ screenshotsDir: '__screenshots__' }), + ...playwrightConfig({ + screenshotsDir: '__screenshots__', + expect: { + toHaveScreenshot: { + animations: 'allow', + }, + }, + }), 'a.spec.js': ` pwt.test('is a test', async ({ page }) => { await page.goto('${infiniteAnimationURL}'); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index ec72e84ea0..2726910087 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -54,6 +54,27 @@ type ExpectSettings = { * An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default. */ maxDiffPixelRatio?: number, + /** + * When set to `"disabled"`, 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. + * + * Defaults to `"disabled"` that leaves animations untouched. + */ + animations?: 'allow'|'disabled', + /** + * When set to `"ready"`, screenshot will wait for + * [`document.fonts.ready`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) promise to resolve in all + * frames. Defaults to `"ready"`. + */ + fonts?: 'ready'|'nowait', + /** + * When set to `"css"`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will + * keep screenshots small. Using `"device"` option will produce a single pixel per each device pixel, so screenhots of + * high-dpi devices will be twice as large or even larger. Defaults to `"css"`. + */ + size?: 'css'|'device', } toMatchSnapshot?: { /** An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax). Defaults to `0.2`.