diff --git a/.changeset/perfect-geese-burn.md b/.changeset/perfect-geese-burn.md new file mode 100644 index 0000000..9bb9caa --- /dev/null +++ b/.changeset/perfect-geese-burn.md @@ -0,0 +1,5 @@ +--- +"@quickpickle/playwright": minor +--- + +added screenshot options to playwright world config diff --git a/packages/playwright/screenshots/Feature: Basic tests of Playwright browser and steps_Setting a clip area_03.png b/packages/playwright/screenshots/Feature: Basic tests of Playwright browser and steps_Setting a clip area_03.png new file mode 100644 index 0000000..05358a8 Binary files /dev/null and b/packages/playwright/screenshots/Feature: Basic tests of Playwright browser and steps_Setting a clip area_03.png differ diff --git a/packages/playwright/screenshots/Feature: Basic tests of Playwright browser and steps_Setting a screenshot mask_03.png b/packages/playwright/screenshots/Feature: Basic tests of Playwright browser and steps_Setting a screenshot mask_03.png new file mode 100644 index 0000000..279df17 Binary files /dev/null and b/packages/playwright/screenshots/Feature: Basic tests of Playwright browser and steps_Setting a screenshot mask_03.png differ diff --git a/packages/playwright/src/PlaywrightWorld.ts b/packages/playwright/src/PlaywrightWorld.ts index 78357a7..4091fed 100644 --- a/packages/playwright/src/PlaywrightWorld.ts +++ b/packages/playwright/src/PlaywrightWorld.ts @@ -8,6 +8,7 @@ import { InfoConstructor } from 'quickpickle/dist/world'; import path from 'node:path' import url from 'node:url' import { expect } from 'playwright/test'; +import { ScreenshotSetting } from './snapshotMatcher'; export const projectRoot = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)).replace(/node_modules.+/, ''), '..') const browsers = { chromium, firefox, webkit } @@ -16,6 +17,7 @@ export type PlaywrightWorldConfigSetting = Partial<{ host: string, // default host, including protocol (default: http://localhost) port: number, // port to which the browser should connect (default: undefined) screenshotDir: string, // directory in which to save screenshots (default: "screenshots") + screenshotOptions?: ScreenshotSetting // options for the default screenshot comparisons nojsTags: string|string[] // tags for scenarios to run without javascript (default: @nojs, @noscript) showBrowserTags: string|string[] // tags for scenarios to run with browser visible (default: @browser, @show-browser, @showbrowser) slowMoTags: string|string[] // tags for scenarios to be run with slow motion enabled (default: @slowmo) @@ -97,7 +99,7 @@ export class PlaywrightWorld extends QuickPickleWorld { } setConfig(worldConfig:PlaywrightWorldConfigSetting) { - let newConfig = defaultsDeep(worldConfig || {}, defaultPlaywrightWorldConfig ) + let newConfig = defaultsDeep(worldConfig || {}, defaultPlaywrightWorldConfig) newConfig.nojsTags = normalizeTags(newConfig.nojsTags) newConfig.showBrowserTags = normalizeTags(newConfig.showBrowserTags) newConfig.slowMoTags = normalizeTags(newConfig.slowMoTags) @@ -108,7 +110,6 @@ export class PlaywrightWorld extends QuickPickleWorld { } this.info.config.worldConfig = newConfig } - async setViewportSize(size?:string) { if (size) { size = size.replace(/^['"]/, '').replace(/['"]$/, '') @@ -133,6 +134,7 @@ export class PlaywrightWorld extends QuickPickleWorld { } async reset(conf?:PlaywrightWorldConfigSetting) { + let url = this.page.url() || this.baseUrl.toString() await this.page?.close() await this.browserContext?.close() if (conf) { @@ -144,6 +146,7 @@ export class PlaywrightWorld extends QuickPickleWorld { serviceWorkers: 'block' }) this.page = await this.browserContext.newPage() + await this.page.goto(url, { timeout: this.worldConfig.stepTimeout }) } async close() { @@ -318,6 +321,17 @@ export class PlaywrightWorld extends QuickPickleWorld { } } + async screenshot(opts?:{ + name?:string + locator?:Locator + }) { + let explodedTags = this.info.explodedIdx ? `_(${this.info.tags.join(',')})` : '' + let path = opts?.name ? this.sanitizeFilepath(`${this.projectRoot}/${this.worldConfig.screenshotDir}/${opts.name}${explodedTags}.png`) : this.fullScreenshotPath + let locator = opts?.locator ?? this.page + console.log('FWAH') + return await locator.screenshot({ path, ...this.worldConfig.screenshotOpts }) + } + } function getDimensions(size:string) { diff --git a/packages/playwright/src/actions.steps.ts b/packages/playwright/src/actions.steps.ts index 1aab7ea..618de06 100644 --- a/packages/playwright/src/actions.steps.ts +++ b/packages/playwright/src/actions.steps.ts @@ -155,22 +155,18 @@ When('I scroll down/up/left/right {int}(px)( pixels)', async function (world:Pla // Screenshots Then('(I )take (a )screenshot', async function (world:PlaywrightWorld) { - await world.page.screenshot({ path:world.fullScreenshotPath }) + await world.screenshot() }) Then('(I )take (a )screenshot named {string}', async function (world:PlaywrightWorld, name:string) { - let explodedTags = world.info.explodedIdx ? `_(${world.info.tags.join(',')})` : '' - let path = world.sanitizeFilepath(`${world.projectRoot}/${world.worldConfig.screenshotDir}/${name}${explodedTags}.png`) - await world.page.screenshot({ path }) + await world.screenshot({ name }) }) Then('(I )take (a )screenshot of the {string} {word}', async function (world:PlaywrightWorld, identifier:string, role:string) { let locator = world.getLocator(world.page, identifier, role) - await locator.screenshot({ path:world.fullScreenshotPath }) + await world.screenshot({ locator }) }) Then('(I )take (a )screenshot of the {string} {word} named {string}', async function (world:PlaywrightWorld, identifier:string, role:string, name:string) { let locator = world.getLocator(world.page, identifier, role) - let explodedTags = world.info.explodedIdx ? `_(${world.info.tags.join(',')})` : '' - let path = world.sanitizeFilepath(`${world.projectRoot}/${world.worldConfig.screenshotDir}/${name}${explodedTags}.png`) - await locator.screenshot({ path }) + await world.screenshot({ locator, name }) }) // ================ diff --git a/packages/playwright/src/outcomes.steps.ts b/packages/playwright/src/outcomes.steps.ts index 5f63bc4..58ed017 100644 --- a/packages/playwright/src/outcomes.steps.ts +++ b/packages/playwright/src/outcomes.steps.ts @@ -190,20 +190,20 @@ Then('the meta( )tag {string} should not/NOT contain/include/be/equal {string}', // Visual regression testing Then('(the )screenshot should match', async function (world:PlaywrightWorld) { - await expect(world.page).toMatchScreenshot(world.screenshotPath) + await expect(world.page).toMatchScreenshot(world.screenshotPath, world.worldConfig.screenshotOptions) }) Then('(the )screenshot {string} should match', async function (world:PlaywrightWorld, name:string) { let explodedTags = world.info.explodedIdx ? `_(${world.info.tags.join(',')})` : '' - await expect(world.page).toMatchScreenshot(`${world.worldConfig.screenshotDir}/${name}${explodedTags}.png`) + await expect(world.page).toMatchScreenshot(`${world.worldConfig.screenshotDir}/${name}${explodedTags}.png`, world.worldConfig.screenshotOptions) }) Then('(the )screenshot of the {string} {word} should match', async function (world:PlaywrightWorld, identifier, role) { let locator = await world.getLocator(world.page, identifier, role) - await expect(locator).toMatchScreenshot(world.screenshotPath) + await expect(locator).toMatchScreenshot(world.screenshotPath, world.worldConfig.screenshotOptions) }) Then('(the )screenshot {string} of the {string} {word} should match', async function (world:PlaywrightWorld, name, identifier, role) { let locator = await world.getLocator(world.page, identifier, role) let explodedTags = world.info.explodedIdx ? `_(${world.info.tags.join(',')})` : '' - await expect(locator).toMatchScreenshot(`${world.worldConfig.screenshotDir}/${name}${explodedTags}.png`) + await expect(locator).toMatchScreenshot(`${world.worldConfig.screenshotDir}/${name}${explodedTags}.png`, world.worldConfig.screenshotOptions) }) // Browser context diff --git a/packages/playwright/src/snapshotMatcher.ts b/packages/playwright/src/snapshotMatcher.ts index 57a15f1..39edebd 100644 --- a/packages/playwright/src/snapshotMatcher.ts +++ b/packages/playwright/src/snapshotMatcher.ts @@ -5,6 +5,7 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import pixelmatch, { PixelmatchOptions } from 'pixelmatch'; import { defaultsDeep } from 'lodash-es'; +import type { PageScreenshotOptions } from 'playwright-core'; export interface ToHaveScreenshotOptions extends PixelmatchOptions { @@ -19,14 +20,29 @@ export interface ToHaveScreenshotOptions extends PixelmatchOptions { height: number; }; fullPage?: boolean; - mask?: Array; + mask?: Array; maskColor?: string; omitBackground?: boolean; timeout?: number; }; -const defaultOptions = { +export interface ScreenshotSetting extends PixelmatchOptions { + maxDiffPercentage?: number; + clip?: { + x?: number + y?: number + width?: number + height?: number + } + fullPage?: boolean + mask?: string[] + maskColor?: string + omitBackground?: boolean + timeout?: number +} + +export const defaultScreenshotOptions = { // Options for the comparison maxDiffPercentage: 0, @@ -34,14 +50,14 @@ const defaultOptions = { fullPage: true, omitBackground: false, mask: [], - maskColor: 'rgb(255,255,255)', + maskColor: '#555555', timeout: 5000, // Options for pixelmatch threshold: 0.1, alpha:0.6, }; -type _DefaultOptions = Omit & { mask: Array }; +type _DefaultOptions = Omit & { mask: Array }; type _ToHaveScreenshotOptions = Omit & _DefaultOptions; async function compareImages(actual: Buffer, expected: Buffer, opts:_ToHaveScreenshotOptions): Promise<{ pass: boolean; diffPercentage: number, image: PNG }> { @@ -65,7 +81,7 @@ async function customToHaveScreenshot( snapshotPath: string, opts?: Partial, ): Promise<{ pass: boolean; message: () => string }> { - const options = defaultsDeep(opts, defaultOptions); + const options = defaultsDeep(opts, defaultScreenshotOptions); const pathparts = snapshotPath.split('/'); const name = pathparts.pop(); const screenshotDir = pathparts.join('/'); @@ -128,6 +144,11 @@ playwrightExpect.extend({ async toMatchScreenshot(received: Page | Locator, nameOrOptions?: string | Partial, optOptions?: Partial) { const name = typeof nameOrOptions === 'string' ? nameOrOptions : 'screenshot'; const options = typeof nameOrOptions === 'object' ? nameOrOptions : optOptions; + if (options?.mask) { + options.mask = options.mask.map((stringOrLocator) => { + return typeof stringOrLocator !== 'string' ? stringOrLocator : received.locator(stringOrLocator); + }); + } return customToHaveScreenshot.call(this, received, name, options); }, }); diff --git a/packages/playwright/tests/playwright.feature b/packages/playwright/tests/playwright.feature index 9414dda..3bae04b 100644 --- a/packages/playwright/tests/playwright.feature +++ b/packages/playwright/tests/playwright.feature @@ -68,6 +68,31 @@ Feature: Basic tests of Playwright browser and steps Then the file "screenshots/visual-regression-simple-page.png.diff.png" should not exist And the file "screenshots/visual-regression-simple-page.png.actual.png" should exist--delete it + Rule: Setting screenshot options must be supported + + Scenario: Setting a screenshot mask + Given I load the file "tests/examples/example.html" + And the following world config: + ```yaml + screenshotOptions: + mask: + - form + ``` + Then the screenshot should match + + Scenario: Setting a clip area + Given I load the file "tests/examples/example.html" + And the following world config: + ```yaml + screenshotOptions: + clip: + x: 0 + y: 60 + width: 300 + height: 180 + ``` + Then the screenshot should match + @skip-ci Rule: Screenshots should be placed in the proper directory diff --git a/packages/playwright/tests/playwright.steps.ts b/packages/playwright/tests/playwright.steps.ts index 025ae7a..e935176 100644 --- a/packages/playwright/tests/playwright.steps.ts +++ b/packages/playwright/tests/playwright.steps.ts @@ -1,5 +1,5 @@ import { Given, When, Then, DocString, DataTable, AfterAll } from 'quickpickle' -import type { PlaywrightWorld } from '../src/PlaywrightWorld' +import type { PlaywrightWorld, PlaywrightWorldConfig, PlaywrightWorldConfigSetting } from '../src/PlaywrightWorld' import yaml from 'js-yaml' import { expect } from '@playwright/test' @@ -9,15 +9,18 @@ export const projectRoot = path.resolve(path.dirname(url.fileURLToPath(import.me import fs from 'fs' Given('the following world config:', async (world:PlaywrightWorld, rawConf:DocString|DataTable) => { + let config:PlaywrightWorldConfigSetting if (rawConf instanceof DataTable) - world.reset(rawConf.rowsHash()) + config = rawConf.rowsHash() else if (rawConf.mediaType.match(/^json/)) - world.reset(JSON.parse(rawConf.toString())) + config = JSON.parse(rawConf.toString()) else if (rawConf.mediaType.match(/^ya?ml$/)) - world.reset(yaml.load(rawConf.toString())) + config = yaml.load(rawConf.toString()) else if (rawConf.match(/\s*\{/)) - world.reset(JSON.parse(rawConf.toString())) - else world.reset(yaml.load(rawConf.toString())) + config = JSON.parse(rawConf.toString()) + else config = yaml.load(rawConf.toString()) + await world.reset(config) + console.log('world config', world.worldConfig) }) // Filesystem