Skip to content

Commit 3dbf0e4

Browse files
committed
chore: write tests
Signed-off-by: Jakub Freisler <[email protected]>
1 parent 4ce389a commit 3dbf0e4

File tree

8 files changed

+139
-67
lines changed

8 files changed

+139
-67
lines changed

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"serve": "vue-cli-service serve",
77
"build": "vue-cli-service build",
8-
"test:open": "vue-cli-service test:e2e --env \"pluginVisualRegressionImagesPath={spec_path}/__image_snapshots_locals__\"",
8+
"test:open": "vue-cli-service test:e2e --env \"pluginVisualRegressionImagesPath={spec_path}/__image_snapshots_local__\"",
99
"test:run": "vue-cli-service test:e2e",
1010
"test:ct": "yarn test:open --component",
1111
"test:ct:ci": "yarn test:run --component --headless",

src/afterScreenshot.hook.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { it, expect, describe } from "vitest";
22
import path from "path";
33
import { promises as fs, existsSync } from "fs";
4-
import { initAfterScreenshotHook } from "./afterScreenshot.hook";
4+
import { initAfterScreenshotHook, parseAbsolutePath } from "./afterScreenshot.hook";
55
import { dir, file, setGracefulCleanup } from "tmp-promise";
6-
import { IMAGE_SNAPSHOT_PREFIX } from "./constants";
6+
import { IMAGE_SNAPSHOT_PREFIX, PATH_VARIABLES } from "./constants";
77

88
setGracefulCleanup();
99

@@ -31,3 +31,23 @@ describe("initAfterScreenshotHook", () => {
3131
await fs.unlink(expectedNewPath);
3232
});
3333
});
34+
35+
describe('parseAbsolutePath', () => {
36+
const projectRoot = '/its/project/root';
37+
38+
it('resolves relative paths against project root', () => {
39+
expect(parseAbsolutePath({ screenshotPath: 'some/path.png', projectRoot }))
40+
.toBe('/its/project/root/some/path.png');
41+
});
42+
43+
it('builds proper win paths when found', () => {
44+
expect(parseAbsolutePath({ screenshotPath: `${PATH_VARIABLES.winSystemRootPath}/D/some/path.png`, projectRoot }))
45+
// that's expected output accorind to https://stackoverflow.com/a/64135721/8805801
46+
.toBe('D:\\/some/path.png');
47+
});
48+
49+
it('resolves relative paths against project root', () => {
50+
expect(parseAbsolutePath({ screenshotPath: `${PATH_VARIABLES.unixSystemRootPath}/some/path.png`, projectRoot }))
51+
.toBe('/some/path.png');
52+
});
53+
});

src/afterScreenshot.hook.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const getConfigVariableOrThrow = <K extends keyof Cypress.PluginConfigOptions>(
2525
};
2626
/* c8 ignore stop */
2727

28-
const parseAbsolutePath = ({
28+
export const parseAbsolutePath = ({
2929
screenshotPath,
3030
projectRoot,
3131
}: {

src/commands.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ declare global {
3030
}
3131
}
3232

33-
const nameCacheCounter: Record<string, number> = {};
34-
3533
const constructCypressError = (log: Cypress.Log, err: Error) => {
3634
// only way to throw & log the message properly in Cypress
3735
// https://github.com/cypress-io/cypress/blob/5f94cad3cb4126e0567290b957050c33e3a78e3c/packages/driver/src/cypress/error_utils.ts#L214-L216
@@ -100,10 +98,7 @@ Cypress.Commands.add(
10098
{ prevSubject: "optional" },
10199
(subject, options = {}) => {
102100
const $el = subject as JQuery<HTMLElement> | undefined;
103-
let title = options.title || Cypress.currentTest.titlePath.join(" ");
104-
if (typeof nameCacheCounter[title] === "undefined")
105-
nameCacheCounter[title] = -1;
106-
title += ` #${++nameCacheCounter[title]}`;
101+
let title: string;
107102

108103
const {
109104
scaleFactor,
@@ -116,17 +111,19 @@ Cypress.Commands.add(
116111

117112
return cy
118113
.then(() =>
119-
cy.task<string>(
120-
TASK.getScreenshotPath,
114+
cy.task<{ screenshotPath: string; title: string }>(
115+
TASK.getScreenshotPathInfo,
121116
{
122-
title,
117+
titleFromOptions:
118+
options.title || Cypress.currentTest.titlePath.join(" "),
123119
imagesPath,
124120
specPath: Cypress.spec.relative,
125121
},
126122
{ log: false }
127123
)
128124
)
129-
.then((screenshotPath) => {
125+
.then(({ screenshotPath, title: titleFromTask }) => {
126+
title = titleFromTask;
130127
let imgPath: string;
131128
return ($el ? cy.wrap($el) : cy)
132129
.screenshot(screenshotPath, {

src/constants.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ export enum FILE_SUFFIX {
99
}
1010

1111
export const TASK = {
12-
getScreenshotPath: `${PLUGIN_NAME}-getScreenshotPath`,
12+
getScreenshotPathInfo: `${PLUGIN_NAME}-getScreenshotPathInfo`,
1313
compareImages: `${PLUGIN_NAME}-compareImages`,
1414
approveImage: `${PLUGIN_NAME}-approveImage`,
15+
cleanupImages: `${PLUGIN_NAME}-cleanupImages`,
1516
doesFileExist: `${PLUGIN_NAME}-doesFileExist`,
1617
/* c8 ignore next */
1718
};
@@ -20,6 +21,8 @@ export const PATH_VARIABLES = {
2021
specPath: "{spec_path}",
2122
unixSystemRootPath: "{unix_system_root_path}",
2223
winSystemRootPath: "{win_system_root_path}",
23-
};
24+
} as const;
2425

2526
export const WINDOWS_LIKE_DRIVE_REGEX = /^[A-Z]:$/;
27+
28+
export const METADATA_KEY = "FRSOURCE_CPVRD_V";

src/screenshotPath.utils.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
import path from "path";
2-
import { FILE_SUFFIX, IMAGE_SNAPSHOT_PREFIX } from "./constants";
2+
import { FILE_SUFFIX, IMAGE_SNAPSHOT_PREFIX, PATH_VARIABLES, WINDOWS_LIKE_DRIVE_REGEX } from "./constants";
33
import sanitize from "sanitize-filename";
44

55
const nameCacheCounter: Record<string, number> = {};
66

77
export const generateScreenshotPath = ({
88
titleFromOptions,
9-
imagesDir,
9+
imagesPath,
1010
specPath,
1111
}: {
1212
titleFromOptions: string;
13-
imagesDir: string;
13+
imagesPath: string;
1414
specPath: string;
1515
}) => {
16+
const parsePathPartVariables = (pathPart: string, i: number) => {
17+
if (pathPart === PATH_VARIABLES.specPath) {
18+
return path.dirname(specPath);
19+
} else if (i === 0 && !pathPart) {
20+
// when unix-like absolute path
21+
return PATH_VARIABLES.unixSystemRootPath;
22+
} else if (i === 0 && WINDOWS_LIKE_DRIVE_REGEX.test(pathPart)) {
23+
// when win-like absolute path
24+
return path.join(PATH_VARIABLES.winSystemRootPath, pathPart[0]);
25+
}
26+
27+
return pathPart;
28+
};
29+
1630
const screenshotPath = path.join(
17-
path.dirname(specPath),
18-
...imagesDir.split("/"),
31+
...imagesPath.split("/").map(parsePathPartVariables),
1932
sanitize(titleFromOptions)
2033
);
2134

src/task.hook.test.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,69 @@ const writeTmpFixture = async (pathToWriteTo: string, fixtureName: string) => {
4040
};
4141

4242
describe("getScreenshotPathInfoTask", () => {
43+
const specPath = "some/nested/spec-path/spec.ts";
44+
4345
it("returns sanitized path and title", () => {
4446
expect(
4547
getScreenshotPathInfoTask({
4648
titleFromOptions: "some-title-withśpęćiał人物",
47-
imagesDir: "nested/images/dir",
48-
specPath: "some/nested/spec-path/spec.ts",
49+
imagesPath: "nested/images/dir",
50+
specPath,
4951
})
5052
).toEqual({
5153
screenshotPath:
52-
"__cp-visual-regression-diff_snapshots__/some/nested/spec-path/nested/images/dir/some-title-withśpęćiał人物 #0.actual.png",
54+
"__cp-visual-regression-diff_snapshots__/nested/images/dir/some-title-withśpęćiał人物 #0.actual.png",
5355
title: "some-title-withśpęćiał人物 #0.actual",
5456
});
5557
});
58+
59+
it("supports {spec_path} variable", () => {
60+
expect(
61+
getScreenshotPathInfoTask({
62+
titleFromOptions: "some-title",
63+
imagesPath: "{spec_path}/images/dir",
64+
specPath,
65+
})
66+
).toEqual({
67+
screenshotPath:
68+
"__cp-visual-regression-diff_snapshots__/some/nested/spec-path/images/dir/some-title #0.actual.png",
69+
title: 'some-title #0.actual',
70+
});
71+
});
72+
73+
it("supports OS-specific absolute paths", () => {
74+
expect(
75+
getScreenshotPathInfoTask({
76+
titleFromOptions: "some-title",
77+
imagesPath: "/images/dir",
78+
specPath,
79+
})
80+
).toEqual({
81+
screenshotPath:
82+
"__cp-visual-regression-diff_snapshots__/{unix_system_root_path}/images/dir/some-title #0.actual.png",
83+
title: 'some-title #0.actual',
84+
});
85+
86+
expect(
87+
getScreenshotPathInfoTask({
88+
titleFromOptions: "some-title",
89+
imagesPath: "C:/images/dir",
90+
specPath,
91+
})
92+
).toEqual({
93+
screenshotPath:
94+
"__cp-visual-regression-diff_snapshots__/{win_system_root_path}/C/images/dir/some-title #0.actual.png",
95+
title: 'some-title #0.actual',
96+
});
97+
});
5698
});
5799

58100
describe("cleanupImagesTask", () => {
59101
describe("when env is set", () => {
60102
const generateUsedScreenshotPath = async (projectRoot: string) => {
61103
const screenshotPathWithPrefix = generateScreenshotPath({
62104
titleFromOptions: "some-file",
63-
imagesDir: "images",
105+
imagesPath: "images",
64106
specPath: "some/spec/path",
65107
});
66108
return path.join(

src/task.hook.ts

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import fs from "fs";
2-
import path from "path";
32
import { PNG } from "pngjs";
43
import pixelmatch, { PixelmatchOptions } from "pixelmatch";
54
import moveFile from "move-file";
6-
import sanitize from "sanitize-filename";
5+
import path from "path";
6+
import { FILE_SUFFIX, IMAGE_SNAPSHOT_PREFIX, TASK } from "./constants";
77
import {
8-
FILE_SUFFIX,
9-
IMAGE_SNAPSHOT_PREFIX,
10-
TASK,
11-
PATH_VARIABLES,
12-
WINDOWS_LIKE_DRIVE_REGEX,
13-
} from "./constants";
14-
import { alignImagesToSameSize, importAndScaleImage } from "./image.utils";
8+
cleanupUnused,
9+
alignImagesToSameSize,
10+
scaleImageAndWrite,
11+
isImageCurrentVersion,
12+
writePNG,
13+
} from "./image.utils";
14+
import { generateScreenshotPath } from "./screenshotPath.utils";
1515
import type { CompareImagesTaskReturn } from "./types";
1616

1717
export type CompareImagesCfg = {
@@ -31,34 +31,22 @@ const unlinkSyncSafe = (path: string) =>
3131
const moveSyncSafe = (pathFrom: string, pathTo: string) =>
3232
fs.existsSync(pathFrom) && moveFile.sync(pathFrom, pathTo);
3333

34-
export const getScreenshotPathTask = ({
35-
title,
36-
imagesPath,
37-
specPath,
38-
}: {
39-
title: string;
34+
export const getScreenshotPathInfoTask = (cfg: {
35+
titleFromOptions: string;
4036
imagesPath: string;
4137
specPath: string;
4238
}) => {
43-
const parsePathPartVariables = (pathPart: string, i: number) => {
44-
if (pathPart === PATH_VARIABLES.specPath) {
45-
return path.dirname(specPath);
46-
} else if (i === 0 && !pathPart) {
47-
// when unix-like absolute path
48-
return PATH_VARIABLES.unixSystemRootPath;
49-
} else if (i === 0 && WINDOWS_LIKE_DRIVE_REGEX.test(pathPart)) {
50-
// when win-like absolute path
51-
return path.join(PATH_VARIABLES.winSystemRootPath, pathPart[0]);
52-
}
39+
const screenshotPath = generateScreenshotPath(cfg);
5340

54-
return pathPart;
55-
};
41+
return { screenshotPath, title: path.basename(screenshotPath, ".png") };
42+
};
43+
44+
export const cleanupImagesTask = (config: Cypress.PluginConfigOptions) => {
45+
if (config.env["pluginVisualRegressionCleanupUnusedImages"]) {
46+
cleanupUnused(config.projectRoot);
47+
}
5648

57-
return path.join(
58-
IMAGE_SNAPSHOT_PREFIX,
59-
...imagesPath.split("/").map(parsePathPartVariables),
60-
`${sanitize(title)}${FILE_SUFFIX.actual}.png`
61-
);
49+
return null;
6250
};
6351

6452
export const approveImageTask = ({ img }: { img: string }) => {
@@ -77,16 +65,18 @@ export const compareImagesTask = async (
7765
cfg: CompareImagesCfg
7866
): Promise<CompareImagesTaskReturn> => {
7967
const messages = [] as string[];
68+
const rawImgNewBuffer = await scaleImageAndWrite({
69+
scaleFactor: cfg.scaleFactor,
70+
path: cfg.imgNew,
71+
});
8072
let imgDiff: number | undefined;
8173
let imgNewBase64: string, imgOldBase64: string, imgDiffBase64: string;
8274
let error = false;
8375

8476
if (fs.existsSync(cfg.imgOld) && !cfg.updateImages) {
85-
const rawImgNew = await importAndScaleImage({
86-
scaleFactor: cfg.scaleFactor,
87-
path: cfg.imgNew,
88-
});
89-
const rawImgOld = PNG.sync.read(fs.readFileSync(cfg.imgOld));
77+
const rawImgNew = PNG.sync.read(rawImgNewBuffer);
78+
const rawImgOldBuffer = fs.readFileSync(cfg.imgOld);
79+
const rawImgOld = PNG.sync.read(rawImgOldBuffer);
9080
const isImgSizeDifferent =
9181
rawImgNew.height !== rawImgOld.height ||
9282
rawImgNew.width !== rawImgOld.width;
@@ -130,7 +120,7 @@ export const compareImagesTask = async (
130120
imgOldBase64 = PNG.sync.write(imgOld).toString("base64");
131121

132122
if (error) {
133-
fs.writeFileSync(
123+
writePNG(
134124
cfg.imgNew.replace(FILE_SUFFIX.actual, FILE_SUFFIX.diff),
135125
diffBuffer
136126
);
@@ -144,15 +134,21 @@ export const compareImagesTask = async (
144134
maxDiffThreshold: cfg.maxDiffThreshold,
145135
};
146136
} else {
147-
// don't overwrite file if it's the same (imgDiff < cfg.maxDiffThreshold && !isImgSizeDifferent)
148-
fs.unlinkSync(cfg.imgNew);
137+
if (rawImgOld && !isImageCurrentVersion(rawImgOldBuffer)) {
138+
writePNG(cfg.imgNew, rawImgNewBuffer);
139+
moveFile.sync(cfg.imgNew, cfg.imgOld);
140+
} else {
141+
// don't overwrite file if it's the same (imgDiff < cfg.maxDiffThreshold && !isImgSizeDifferent)
142+
fs.unlinkSync(cfg.imgNew);
143+
}
149144
}
150145
} else {
151146
// there is no "old screenshot" or screenshots should be immediately updated
152147
imgDiff = 0;
153148
imgNewBase64 = "";
154149
imgDiffBase64 = "";
155150
imgOldBase64 = "";
151+
writePNG(cfg.imgNew, rawImgNewBuffer);
156152
moveFile.sync(cfg.imgNew, cfg.imgOld);
157153
}
158154

@@ -182,8 +178,9 @@ export const doesFileExistTask = ({ path }: { path: string }) =>
182178
fs.existsSync(path);
183179

184180
/* c8 ignore start */
185-
export const initTaskHook = () => ({
186-
[TASK.getScreenshotPath]: getScreenshotPathTask,
181+
export const initTaskHook = (config: Cypress.PluginConfigOptions) => ({
182+
[TASK.getScreenshotPathInfo]: getScreenshotPathInfoTask,
183+
[TASK.cleanupImages]: cleanupImagesTask.bind(undefined, config),
187184
[TASK.doesFileExist]: doesFileExistTask,
188185
[TASK.approveImage]: approveImageTask,
189186
[TASK.compareImages]: compareImagesTask,

0 commit comments

Comments
 (0)