Skip to content

Commit

Permalink
feat: add screenshot options to playwright world config
Browse files Browse the repository at this point in the history
  • Loading branch information
dnotes committed Dec 16, 2024
1 parent 4ce1543 commit a614266
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-geese-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@quickpickle/playwright": minor
---

added screenshot options to playwright world config
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 16 additions & 2 deletions packages/playwright/src/PlaywrightWorld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -108,7 +110,6 @@ export class PlaywrightWorld extends QuickPickleWorld {
}
this.info.config.worldConfig = newConfig
}

async setViewportSize(size?:string) {
if (size) {
size = size.replace(/^['"]/, '').replace(/['"]$/, '')
Expand All @@ -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) {
Expand All @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down
12 changes: 4 additions & 8 deletions packages/playwright/src/actions.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
})

// ================
Expand Down
8 changes: 4 additions & 4 deletions packages/playwright/src/outcomes.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 26 additions & 5 deletions packages/playwright/src/snapshotMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -19,29 +20,44 @@ export interface ToHaveScreenshotOptions extends PixelmatchOptions {
height: number;
};
fullPage?: boolean;
mask?: Array<Locator>;
mask?: Array<Locator|string>;
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,

// Options for the screenshot
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<typeof defaultOptions, "mask"> & { mask: Array<Locator> };
type _DefaultOptions = Omit<typeof defaultScreenshotOptions, "mask"> & { mask: Array<Locator> };
type _ToHaveScreenshotOptions = Omit<ToHaveScreenshotOptions, keyof _DefaultOptions> & _DefaultOptions;

async function compareImages(actual: Buffer, expected: Buffer, opts:_ToHaveScreenshotOptions): Promise<{ pass: boolean; diffPercentage: number, image: PNG }> {
Expand All @@ -65,7 +81,7 @@ async function customToHaveScreenshot(
snapshotPath: string,
opts?: Partial<ToHaveScreenshotOptions>,
): 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('/');
Expand Down Expand Up @@ -128,6 +144,11 @@ playwrightExpect.extend({
async toMatchScreenshot(received: Page | Locator, nameOrOptions?: string | Partial<ToHaveScreenshotOptions>, optOptions?: Partial<ToHaveScreenshotOptions>) {
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);
},
});
25 changes: 25 additions & 0 deletions packages/playwright/tests/playwright.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 9 additions & 6 deletions packages/playwright/tests/playwright.steps.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
Expand Down

0 comments on commit a614266

Please sign in to comment.