/** * Copyright 2018 Google Inc. All rights reserved. * Modifications copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { test as it, expect } from './pageTest'; import { verifyViewport, attachFrame } from '../config/utils'; import type { Route } from 'playwright-core'; import path from 'path'; import fs from 'fs'; import os from 'os'; it.describe('page screenshot', () => { it.skip(({ browserName, headless }) => browserName === 'firefox' && !headless, 'Firefox headed produces a different image.'); it.skip(({ isAndroid }) => isAndroid, 'Different viewport'); it('should work @smoke', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const screenshot = await page.screenshot(); expect(screenshot).toMatchSnapshot('screenshot-sanity.png'); }); it('should not capture blinking caret by default', async ({ page, server }) => { await page.setContent(`
`); const div = page.locator('div'); await div.type('foo bar'); const screenshot = await div.screenshot(); for (let i = 0; i < 10; ++i) { // Caret blinking time is set to 500ms. // Try to capture variety of screenshots to make // sure we don't capture blinking caret. await new Promise(x => setTimeout(x, 150)); const newScreenshot = await div.screenshot(); expect(newScreenshot.equals(screenshot)).toBe(true); } }); it('should capture blinking caret if explicitly asked for', async ({ page, server }) => { await page.setContent(` `); const div = page.locator('div'); await div.type('foo bar'); const screenshot = await div.screenshot(); let hasDifferentScreenshots = false; for (let i = 0; !hasDifferentScreenshots && i < 10; ++i) { // Caret blinking time is set to 500ms. // Try to capture variety of screenshots to make // sure we capture blinking caret. await new Promise(x => setTimeout(x, 150)); const newScreenshot = await div.screenshot({ caret: 'initial' }); hasDifferentScreenshots = !newScreenshot.equals(screenshot); } expect(hasDifferentScreenshots).toBe(true); }); it('should clip rect', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const screenshot = await page.screenshot({ clip: { x: 50, y: 100, width: 150, height: 100 } }); expect(screenshot).toMatchSnapshot('screenshot-clip-rect.png'); }); it('should clip rect with fullPage', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); await page.evaluate(() => window.scrollBy(150, 200)); const screenshot = await page.screenshot({ fullPage: true, clip: { x: 50, y: 100, width: 150, height: 100, }, }); expect(screenshot).toMatchSnapshot('screenshot-clip-rect.png'); }); it('should clip elements to the viewport', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const screenshot = await page.screenshot({ clip: { x: 50, y: 450, width: 1000, height: 100 } }); expect(screenshot).toMatchSnapshot('screenshot-offscreen-clip.png'); }); it('should throw on clip outside the viewport', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const screenshotError = await page.screenshot({ clip: { x: 50, y: 650, width: 100, height: 100 } }).catch(error => error); expect(screenshotError.message).toContain('Clipped area is either empty or outside the resulting image'); }); it('should run in parallel', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const promises = []; for (let i = 0; i < 3; ++i) { promises.push(page.screenshot({ clip: { x: 50 * i, y: 0, width: 50, height: 50 } })); } const screenshots = await Promise.all(promises); expect(screenshots[1]).toMatchSnapshot('grid-cell-1.png'); }); it('should take fullPage screenshots', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('screenshot-grid-fullpage.png'); }); it('should restore viewport after fullPage screenshot', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toBeInstanceOf(Buffer); await verifyViewport(page, 500, 500); }); it('should allow transparency', async ({ page, browserName }) => { it.fail(browserName === 'firefox'); await page.setViewportSize({ width: 50, height: 150 }); await page.setContent(` `); const screenshot = await page.screenshot({ omitBackground: true }); expect(screenshot).toMatchSnapshot('transparent.png'); }); it('should render white background on jpeg file', async ({ page, server, isElectron }) => { it.fixme(isElectron, 'omitBackground with jpeg does not work'); await page.setViewportSize({ width: 100, height: 100 }); await page.goto(server.EMPTY_PAGE); const screenshot = await page.screenshot({ omitBackground: true, type: 'jpeg' }); expect(screenshot).toMatchSnapshot('white.jpg'); }); it('should work with odd clip size on Retina displays', async ({ page, isElectron }) => { it.fixme(isElectron, 'Scale is wrong'); const screenshot = await page.screenshot({ clip: { x: 0, y: 0, width: 11, height: 11, } }); expect(screenshot).toMatchSnapshot('screenshot-clip-odd-size.png'); }); it('should work for canvas', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/screenshots/canvas.html'); const screenshot = await page.screenshot(); expect(screenshot).toMatchSnapshot('screenshot-canvas.png', { threshold: 0.4 }); }); it('should capture canvas changes', async ({ page, isElectron, browserName, isMac }) => { it.fail(browserName === 'webkit' && isMac && parseInt(os.release(), 10) <= 20, 'https://github.com/microsoft/playwright/issues/8796'); it.skip(isElectron); await page.goto('data:text/html,'); await page.evaluate(() => { const canvas = document.querySelector('canvas'); canvas.width = 600; canvas.height = 600; }); async function addLine(step: number) { await page.evaluate(n => { const canvas = document.querySelector('canvas'); const ctx = canvas.getContext('2d'); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, n * 100); ctx.lineTo(300, n * 100); ctx.stroke(); }, step); } for (let i = 0; i < 3; i++) { await addLine(i); // With the slight delay WebKit stops reflecting changes in the screenshots on macOS. await new Promise(f => setTimeout(f, 100)); const screenshot = await page.screenshot(); expect(screenshot).toMatchSnapshot(`canvas-changes-${i}.png`); } }); it('should work for webgl', async ({ page, server, browserName }) => { it.fixme(browserName === 'firefox' || browserName === 'webkit'); await page.setViewportSize({ width: 640, height: 480 }); await page.goto(server.PREFIX + '/screenshots/webgl.html'); const screenshot = await page.screenshot(); expect(screenshot).toMatchSnapshot('screenshot-webgl.png'); }); it('should work for translateZ', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/screenshots/translateZ.html'); const screenshot = await page.screenshot(); expect(screenshot).toMatchSnapshot('screenshot-translateZ.png'); }); it('should work while navigating', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/redirectloop1.html'); for (let i = 0; i < 10; i++) { const screenshot = await page.screenshot({ fullPage: true }).catch(e => { if (e.message.includes('Cannot take a screenshot while page is navigating')) return Buffer.from(''); throw e; }); expect(screenshot).toBeInstanceOf(Buffer); } }); it('should work with iframe in shadow', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid-iframe-in-shadow.html'); expect(await page.screenshot()).toMatchSnapshot('screenshot-iframe.png'); }); it('path option should work', async ({ page, server }, testInfo) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const outputPath = testInfo.outputPath('screenshot.png'); await page.screenshot({ path: outputPath }); expect(await fs.promises.readFile(outputPath)).toMatchSnapshot('screenshot-sanity.png'); }); it('path option should create subdirectories', async ({ page, server }, testInfo) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const outputPath = testInfo.outputPath(path.join('these', 'are', 'directories', 'screenshot.png')); await page.screenshot({ path: outputPath }); expect(await fs.promises.readFile(outputPath)).toMatchSnapshot('screenshot-sanity.png'); }); it('path option should detect jpeg', async ({ page, server, isElectron }, testInfo) => { it.fixme(isElectron, 'omitBackground with jpeg does not work'); await page.setViewportSize({ width: 100, height: 100 }); await page.goto(server.EMPTY_PAGE); const outputPath = testInfo.outputPath('screenshot.jpg'); const screenshot = await page.screenshot({ omitBackground: true, path: outputPath }); expect(await fs.promises.readFile(outputPath)).toMatchSnapshot('white.jpg'); expect(screenshot).toMatchSnapshot('white.jpg'); }); it('path option should throw for unsupported mime type', async ({ page }) => { const error = await page.screenshot({ path: 'file.txt' }).catch(e => e); expect(error.message).toContain('path: unsupported mime type "text/plain"'); }); it('quality option should throw for png', async ({ page }) => { const error = await page.screenshot({ quality: 10 }).catch(e => e); expect(error.message).toContain('options.quality is unsupported for the png'); }); it('zero quality option should throw for png', async ({ page }) => { const error = await page.screenshot({ quality: 0, type: 'png' }).catch(e => e); expect(error.message).toContain('options.quality is unsupported for the png'); }); it('should prefer type over extension', async ({ page }, testInfo) => { const outputPath = testInfo.outputPath('file.png'); const buffer = await page.screenshot({ path: outputPath, type: 'jpeg' }); expect([buffer[0], buffer[1], buffer[2]]).toEqual([0xFF, 0xD8, 0xFF]); }); it('should not issue resize event', async ({ page, server }) => { await page.goto(server.PREFIX + '/grid.html'); let resizeTriggered = false; await page.exposeFunction('resize', () => { resizeTriggered = true; }); await page.evaluate(() => { window.addEventListener('resize', () => (window as any).resize()); }); await page.screenshot(); expect(resizeTriggered).toBeFalsy(); }); it('should work with Array deleted', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); await page.evaluate(() => delete window.Array); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('screenshot-grid-fullpage.png'); }); it('should take fullPage screenshots during navigation', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const reloadSeveralTimes = async () => { for (let i = 0; i < 5; ++i) await page.reload(); }; const screenshotSeveralTimes = async () => { for (let i = 0; i < 5; ++i) await page.screenshot({ fullPage: true }); }; await Promise.all([ reloadSeveralTimes(), screenshotSeveralTimes() ]); }); it.describe('mask option', () => { it('should work', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); expect(await page.screenshot({ mask: [ page.locator('div').nth(5) ], })).toMatchSnapshot('mask-should-work.png'); }); it('should work with locator', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const bodyLocator = page.locator('body'); expect(await bodyLocator.screenshot({ mask: [ page.locator('div').nth(5) ], })).toMatchSnapshot('mask-should-work-with-locator.png'); }); it('should work with elementhandle', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const bodyHandle = await page.$('body'); expect(await bodyHandle.screenshot({ mask: [ page.locator('div').nth(5) ], })).toMatchSnapshot('mask-should-work-with-elementhandle.png'); }); it('should mask multiple elements', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); expect(await page.screenshot({ mask: [ page.locator('div').nth(5), page.locator('div').nth(12), ], })).toMatchSnapshot('should-mask-multiple-elements.png'); }); it('should mask inside iframe', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); await attachFrame(page, 'frame1', server.PREFIX + '/grid.html'); await page.addStyleTag({ content: 'iframe { border: none; }' }); expect(await page.screenshot({ mask: [ page.locator('div').nth(5), page.frameLocator('#frame1').locator('div').nth(12), ], })).toMatchSnapshot('should-mask-inside-iframe.png'); }); it('should mask in parallel', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await attachFrame(page, 'frame1', server.PREFIX + '/grid.html'); await attachFrame(page, 'frame2', server.PREFIX + '/grid.html'); await page.addStyleTag({ content: 'iframe { border: none; }' }); const screenshots = await Promise.all([ page.screenshot({ mask: [ page.frameLocator('#frame1').locator('div').nth(1) ], }), page.screenshot({ mask: [ page.frameLocator('#frame2').locator('div').nth(3) ], }), ]); expect(screenshots[0]).toMatchSnapshot('should-mask-in-parallel-1.png'); expect(screenshots[1]).toMatchSnapshot('should-mask-in-parallel-2.png'); }); it('should remove mask after screenshot', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const screenshot1 = await page.screenshot(); await page.screenshot({ mask: [ page.locator('div').nth(1) ], }); const screenshot2 = await page.screenshot(); expect(screenshot1.equals(screenshot2)).toBe(true); }); it('should work when subframe has stalled navigation', async ({ page, server }) => { let cb; const routeReady = new Promise