Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(e2e): store images in webp format #2451

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module.exports = {
roots: ['./tests/'],
testRegex: ['(.+)\\.(spec|test)\\.ts$', '(.+).feature'],
moduleFileExtensions: ['ts', 'js', 'feature'],
setupFilesAfterEnv: ['./setup/image.ts', './setup/scenarios.ts'],
setupFilesAfterEnv: ['./setup/serializers/image.ts', './setup/scenarios.ts'],
reporters: [
['./reporters/reporter.js', {
diffDir: './.diff',
Expand Down
6 changes: 4 additions & 2 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
},
"dependencies": {
"@types/jest-environment-puppeteer": "^5.0.6",
"@types/jest-image-snapshot": "^6.4.0",
"@types/pixelmatch": "^5.2.6",
"@types/puppeteer": "^7.0.4",
"jest-image-snapshot": "^6.4.0",
"jest-puppeteer": "^10.0.1",
"looks-same": "^9.0.0",
"pixelmatch": "^5.3.0",
"puppeteer": "^22.11.1",
"sharp": "^0.33.4",
"stucumber": "^0.19.0"
}
}
4 changes: 0 additions & 4 deletions e2e/setup/image.ts

This file was deleted.

13 changes: 4 additions & 9 deletions e2e/setup/scenarios.screenshot.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import {cucumber} from '../transformer/gherkin';

import type {TestEnv} from './scenarios.env';
import type {MatchImageSnapshotOptions} from 'jest-image-snapshot';

const DIFF_CONFIG: MatchImageSnapshotOptions = {
customDiffDir: '.diff'
};

cucumber.defineRule('take a screenshot', async ({screenshots}: TestEnv) => {
screenshots.push(await page.screenshot());
Expand All @@ -29,14 +24,14 @@ cucumber.defineRule('take a screenshot of the element', async ({elements, screen
screenshots.push(await element.screenshot());
});

cucumber.defineRule('check if the screenshot is exactly equal to the snapshotted version', ({screenshots}: TestEnv) => {
cucumber.defineRule('check if the screenshot is exactly equal to the snapshotted version', async ({screenshots}: TestEnv) => {
if (!screenshots.length) throw new Error('E2E: there is no any screenshot, make sure you have "Take a screenshot" before');
const image = screenshots.pop();
expect(image).toMatchImageSnapshot(DIFF_CONFIG);
expect(image).toMatchImageSnapshot();
});

cucumber.defineRule('check if the screenshot is equal to the snapshotted version', ({screenshots}: TestEnv) => {
cucumber.defineRule('check if the screenshot is equal to the snapshotted version', async ({screenshots}: TestEnv) => {
if (!screenshots.length) throw new Error('E2E: there is no any screenshot, make sure you have "Take a screenshot" before');
const image = screenshots.pop();
expect(image).toMatchImageSnapshot({failureThreshold: 1000, blur: .005, comparisonMethod: 'ssim', failureThresholdType: 'pixel', ...DIFF_CONFIG});
expect(image).toMatchImageSnapshot({threshold: 0.1});
});
6 changes: 6 additions & 0 deletions e2e/setup/serializers/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {SnapshotMatcher} from './image/snapshot-matcher';
import type {SnapshotMatcherOptions} from './image/snapshot-matcher';

expect.extend({async toMatchImageSnapshot(received: Buffer, options: SnapshotMatcherOptions = {}) {
return new SnapshotMatcher(this, received, options).match();
}});
43 changes: 43 additions & 0 deletions e2e/setup/serializers/image/diff-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import sharp from 'sharp';
import type {SharpContext} from './snapshot-matcher';

type DiffBuilderConfig = {
diffPath: string;
img1: SharpContext;
img2: SharpContext;
diffBuffer: Buffer;
};

export class DiffBuilder {

protected imgInfo: {width: number, height: number};

constructor(protected config: DiffBuilderConfig) {
this.imgInfo = this.config.img1.info;
}

public async build(): Promise<void> {
const {diffPath, img1, img2, diffBuffer} = this.config;
const {width, height} = this.imgInfo;
await sharp({
create: {
width: width * 3,
height,
channels: 4,
background: {r: 0, g: 0, b: 0, alpha: 0}
}
})
.composite([
{input: await this.toWebp(img1.data), left: 0, top: 0},
{input: await this.toWebp(diffBuffer), left: width, top: 0},
{input: await this.toWebp(img2.data), left: width * 2, top: 0}
])
.webp()
.toFile(diffPath);
}

protected toWebp(buffer: Buffer): Promise<Buffer> {
const {height, width} = this.imgInfo;
return sharp(buffer, {raw: {width, height, channels: 4}}).webp().toBuffer();
}
}
88 changes: 88 additions & 0 deletions e2e/setup/serializers/image/snapshot-matcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import path from 'path';
import fs from 'fs';
import sharp from 'sharp';
import pixelmatch from 'pixelmatch';

import {DiffBuilder} from './diff-builder';

export type SnapshotMatcherOptions = pixelmatch.PixelmatchOptions;

export type SharpContext = {data: Buffer, info: sharp.OutputInfo};

export class SnapshotMatcher {
protected static readonly DIFF_DIR = path.join(process.cwd(), '.diff');
protected static readonly SNAPSHOT_DIR = path.join(process.cwd(), 'tests', '__image_snapshots__');

protected static readonly defaultOptions: SnapshotMatcherOptions = {
diffMask: false,
alpha: 0,
diffColorAlt: [255, 255, 0],
includeAA: true,
threshold: 0.01
};

protected config: SnapshotMatcherOptions;
protected testName: string;
protected currentImg: sharp.Sharp;
protected shouldUpdateImg: boolean;

constructor(context: jest.MatcherContext, received: Buffer, options: SnapshotMatcherOptions = {}) {
this.testName = context.currentTestName!.replace(/([^a-z0-9]+)/gi, '-').toLowerCase();
this.config = Object.assign({}, SnapshotMatcher.defaultOptions, options);
this.currentImg = sharp(received).webp();
this.shouldUpdateImg = context.snapshotState._updateSnapshot === 'all';
}

public async match(): Promise<jest.CustomMatcherResult> {
this.updateDirectory(SnapshotMatcher.SNAPSHOT_DIR);
const prevImgPath = path.join(SnapshotMatcher.SNAPSHOT_DIR, `${this.testName}.webp`);
if (!fs.existsSync(prevImgPath) || this.shouldUpdateImg) {
await this.currentImg.toFile(prevImgPath);
return this.getMatcherResult(true, `New snapshot was created: ${prevImgPath}`);
}

const prevImg = await this.convertToRaw(prevImgPath);
const currImg = await this.convertToRaw(await this.currentImg.toBuffer());
const diffPath = await this.compareImages(prevImg, currImg);

if (diffPath) return this.getMatcherResult(false, `Image mismatch found: ${diffPath}`);
return this.getMatcherResult(true, `Image is the same as the snapshot: ${prevImgPath}`);
}

protected updateDirectory(dirPath: string): void {
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, {recursive: true});
}

protected getMatcherResult(pass: boolean, message: string): jest.CustomMatcherResult {
return {pass, message: () => message};
}

protected async convertToRaw(input: string | Buffer): Promise<SharpContext> {
return sharp(input).ensureAlpha().raw().toBuffer({resolveWithObject: true});
}

protected async compareImages(prevImg: SharpContext, currImg: SharpContext): Promise<string | undefined> {
let diffPath: string | undefined;
const {width, height} = prevImg.info;
const diffBuffer = Buffer.alloc(width * height * 4);
const numDiffPixel = pixelmatch(prevImg.data, currImg.data, diffBuffer, width, height, this.config);
if (numDiffPixel > width * height * 0.0001) diffPath = await this.saveDiff(prevImg, currImg, diffBuffer);
return diffPath;
}

protected async saveDiff(img1: SharpContext, img2: SharpContext, diffBuffer: Buffer): Promise<string> {
this.updateDirectory(SnapshotMatcher.DIFF_DIR);
const diffPath = path.join(SnapshotMatcher.DIFF_DIR, `${this.testName}-diff.webp`);
await new DiffBuilder({diffPath, img1, img2, diffBuffer}).build();
return diffPath;
}
}

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
export interface Matchers<R> {
toMatchImageSnapshot(options?: SnapshotMatcherOptions): Promise<R>;
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Loading