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 jpeg format #2451

Open
wants to merge 28 commits into
base: main-beta
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3b1570e
test(e2e): store images in webp format
fshovchko Jun 5, 2024
b0c0998
test(e2e): add lossless setting for webp screenshots
fshovchko Jun 6, 2024
051c7c2
test(e2e): drop `jest-image-snapshot` and use `looks-same` instead
fshovchko Jun 13, 2024
eddf260
test(e2e): fix illegal symbols in names
fshovchko Jun 14, 2024
0473d90
Merge branch 'main' into e2e/webp
fshovchko Jun 18, 2024
8425803
test(e2e): match images using pixelmatch
fshovchko Jun 18, 2024
59caec2
test(e2e): add -u flag recognizing
fshovchko Jun 18, 2024
5397fe0
test(e2e): add very small pixel tolerance
fshovchko Jun 20, 2024
fb8be3a
Merge remote-tracking branch 'refs/remotes/origin/main' into e2e/webp
ala-n Aug 19, 2024
9be7d69
test(e2e): update snapshots
ala-n Aug 19, 2024
682a708
test(e2e): add error handling
fshovchko Aug 19, 2024
bcb4900
test(e2e): update dependencies
fshovchko Aug 19, 2024
ce4742a
test(e2e): remove original match error from stacktrace
fshovchko Aug 19, 2024
e1f8bd9
test(e2e): code refactoring
fshovchko Aug 19, 2024
4fbe45d
refactor(e2e): fix build due to puppeteer update, cosmetic updates
ala-n Aug 20, 2024
c488669
refactor(e2e): corrections in initialization process and font rendering
ala-n Aug 20, 2024
4afad63
Merge remote-tracking branch 'refs/remotes/origin/main' into e2e/webp
ala-n Aug 27, 2024
d4d4435
chore(e2e): code refactoring
fshovchko Sep 3, 2024
2b50a9f
Merge branch 'main' into e2e/webp
fshovchko Sep 3, 2024
b95e6e5
chore(e2e): refactor sanitize function
fshovchko Sep 4, 2024
40b19ec
Merge branch 'e2e/webp' of github.com:exadel-inc/esl into e2e/webp
fshovchko Sep 4, 2024
43d4fb6
chore(e2e): code refactoring
fshovchko Sep 11, 2024
e9a7b8c
chore(e2e): code refactoring
fshovchko Sep 11, 2024
b84dd8d
chore(e2e): add name in statBase back
fshovchko Sep 12, 2024
6c7b459
chore(e2e): code refactoring
fshovchko Sep 13, 2024
6e030dd
style(e2e): small test report renderer fix
ala-n Sep 13, 2024
038f0ea
Merge branch 'main-beta' into e2e/webp
ala-n Sep 13, 2024
d261a29
chore(e2e): add peer deps to site module
ala-n Sep 13, 2024
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
10 changes: 6 additions & 4 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",
"puppeteer": "^22.15.0",
"jest-puppeteer": "^10.1.0",
"looks-same": "^9.0.0",
fshovchko marked this conversation as resolved.
Show resolved Hide resolved
"pixelmatch": "^5.3.0",
"puppeteer": "^23.1.0",
"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> {
fshovchko marked this conversation as resolved.
Show resolved Hide resolved
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();
}
}
94 changes: 94 additions & 0 deletions e2e/setup/serializers/image/snapshot-matcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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());

let diffPath;
try {
diffPath = await this.compareImages(prevImg, currImg);
fshovchko marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
return this.getMatcherResult(false, `Error comparing snapshot to image ${prevImgPath}`);
}

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});
}
fshovchko marked this conversation as resolved.
Show resolved Hide resolved

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

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