Skip to content

Commit 4669d5a

Browse files
committed
feat: introduce imagesPath option
allow relative path resolution create special {spec_path} variable allow absolute path resolution (under unix and win systems) refactor afterScreenshot hook to awaits add deprecation message for imagesDir option add docs info deprecate imagesDir option in favor of imagesPath Signed-off-by: Jakub Freisler <[email protected]>
1 parent 38679a7 commit 4669d5a

File tree

6 files changed

+183
-165
lines changed

6 files changed

+183
-165
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,13 @@ cy.matchImage({
174174
// default: false
175175
updateImages: true,
176176
// directory path in which screenshot images will be stored
177-
// image visualiser will normalise path separators depending on OS it's being run within, so always use / for nested paths
178-
// default: '__image_snapshots__'
179-
imagesDir: 'this-might-be-your-custom/maybe-nested-directory',
177+
// relative path are resolved against project root
178+
// absolute paths (both on unix and windows OS) supported
179+
// path separators will be normalised by the plugin depending on OS, you should always use / as path separator, e.g.: C:/my-directory/nested for windows-like drive notation
180+
// There are one special variable available to be used in the path:
181+
// - {spec_path} - relative path leading from project root to the current spec file directory (e.g. `/src/components/my-tested-component`)
182+
// default: '{spec_path}/__image_snapshots__'
183+
imagesPath: 'this-might-be-your-custom/maybe-nested-directory',
180184
// maximum threshold above which the test should fail
181185
// default: 0.01
182186
maxDiffThreshold: 0.1,

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 \"pluginVisualRegressionImagesDir=__image_snapshots_local__\"",
8+
"test:open": "vue-cli-service test:e2e --env \"pluginVisualRegressionImagesPath={spec_path}/__image_snapshots_locals__\"",
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.ts

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import path from "path";
2-
import fs from "fs";
2+
import { promises as fs } from "fs";
33
import moveFile from "move-file";
4-
import { IMAGE_SNAPSHOT_PREFIX } from "./constants";
4+
import { IMAGE_SNAPSHOT_PREFIX, PATH_VARIABLES } from "./constants";
55

66
type NotFalsy<T> = T extends false | null | undefined ? never : T;
77

8+
const MIMIC_ROOT_WIN_REGEX = new RegExp(
9+
`^${PATH_VARIABLES.winSystemRootPath}\\${path.sep}([A-Z])\\${path.sep}`
10+
);
11+
const MIMIC_ROOT_UNIX_REGEX = new RegExp(
12+
`^${PATH_VARIABLES.unixSystemRootPath}\\${path.sep}`
13+
);
14+
815
const getConfigVariableOrThrow = <K extends keyof Cypress.PluginConfigOptions>(
916
config: Cypress.PluginConfigOptions,
1017
name: K
@@ -14,25 +21,33 @@ const getConfigVariableOrThrow = <K extends keyof Cypress.PluginConfigOptions>(
1421
}
1522

1623
/* c8 ignore start */
17-
throw `[Image snapshot] CypressConfig.${name} cannot be missing or \`false\`!`;
24+
throw `[@frsource/cypress-plugin-visual-regression-diff] CypressConfig.${name} cannot be missing or \`false\`!`;
1825
};
1926
/* c8 ignore stop */
2027

21-
const removeScreenshotsDirectory = (
22-
screenshotsFolder: string,
23-
onSuccess: () => void,
24-
onError: (e: Error) => void
25-
) => {
26-
fs.rm(
27-
path.join(screenshotsFolder, IMAGE_SNAPSHOT_PREFIX),
28-
{ recursive: true, force: true },
29-
(err) => {
30-
/* c8 ignore start */
31-
if (err) return onError(err);
32-
/* c8 ignore stop */
33-
onSuccess();
34-
}
35-
);
28+
const parseAbsolutePath = ({
29+
screenshotPath,
30+
projectRoot,
31+
}: {
32+
screenshotPath: string;
33+
projectRoot: string;
34+
}) => {
35+
let newAbsolutePath: string;
36+
const matchedMimicingWinRoot = screenshotPath.match(MIMIC_ROOT_WIN_REGEX);
37+
const matchedMimicingUnixRoot = screenshotPath.match(MIMIC_ROOT_UNIX_REGEX);
38+
if (matchedMimicingWinRoot && matchedMimicingWinRoot[1]) {
39+
const driveLetter = matchedMimicingWinRoot[1];
40+
newAbsolutePath = path.join(
41+
`${driveLetter}:\\`,
42+
screenshotPath.substring(matchedMimicingWinRoot[0].length)
43+
);
44+
} else if (matchedMimicingUnixRoot) {
45+
newAbsolutePath =
46+
path.sep + screenshotPath.substring(matchedMimicingUnixRoot[0].length);
47+
} else {
48+
newAbsolutePath = path.join(projectRoot, screenshotPath);
49+
}
50+
return path.normalize(newAbsolutePath);
3651
};
3752

3853
export const initAfterScreenshotHook =
@@ -47,27 +62,25 @@ export const initAfterScreenshotHook =
4762
/* c8 ignore start */
4863
if (details.name?.indexOf(IMAGE_SNAPSHOT_PREFIX) !== 0) return;
4964
/* c8 ignore stop */
50-
return new Promise((resolve, reject) => {
51-
const screenshotsFolder = getConfigVariableOrThrow(
52-
config,
53-
"screenshotsFolder"
54-
);
65+
const screenshotsFolder = getConfigVariableOrThrow(
66+
config,
67+
"screenshotsFolder"
68+
);
69+
const screenshotPath = details.name.substring(
70+
IMAGE_SNAPSHOT_PREFIX.length + path.sep.length
71+
);
72+
const newAbsolutePath = parseAbsolutePath({
73+
screenshotPath,
74+
projectRoot: config.projectRoot,
75+
});
5576

56-
const newRelativePath = details.name.substring(
57-
IMAGE_SNAPSHOT_PREFIX.length + path.sep.length
58-
);
59-
const newAbsolutePath = path.normalize(
60-
path.join(config.projectRoot, newRelativePath)
61-
);
77+
return (async () => {
78+
await moveFile(details.path, newAbsolutePath);
79+
await fs.rm(path.join(screenshotsFolder, IMAGE_SNAPSHOT_PREFIX), {
80+
recursive: true,
81+
force: true,
82+
});
6283

63-
void moveFile(details.path, newAbsolutePath)
64-
.then(() =>
65-
removeScreenshotsDirectory(
66-
screenshotsFolder,
67-
() => resolve({ path: newAbsolutePath }),
68-
reject
69-
)
70-
)
71-
.catch(reject);
72-
});
84+
return { path: newAbsolutePath };
85+
})();
7386
};

src/commands.ts

Lines changed: 75 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,13 @@ declare global {
1010
screenshotConfig?: Partial<Cypress.ScreenshotDefaultsOptions>;
1111
diffConfig?: Parameters<typeof pixelmatch>[5];
1212
updateImages?: boolean;
13+
/**
14+
* @deprecated since version 3.0, use imagesPath instead
15+
*/
1316
imagesDir?: string;
17+
imagesPath?: string;
1418
maxDiffThreshold?: number;
1519
title?: string;
16-
matchAgainstPath?: string;
17-
// IDEA: to be implemented if support for files NOT from filesystem needed
18-
// matchAgainst?: string | Buffer;
19-
};
20-
21-
type MatchImageReturn = {
22-
diffValue: number | undefined;
23-
imgNewPath: string;
24-
imgPath: string;
25-
imgDiffPath: string;
26-
imgNewBase64: string | undefined;
27-
imgBase64: string | undefined;
28-
imgDiffBase64: string | undefined;
29-
imgNew: InstanceType<Cypress['Buffer']> | undefined;
30-
img: InstanceType<Cypress['Buffer']> | undefined;
31-
imgDiff: InstanceType<Cypress['Buffer']> | undefined;
3220
};
3321

3422
interface Chainable<Subject> {
@@ -37,11 +25,13 @@ declare global {
3725
* @memberof Cypress.Chainable
3826
* @example cy.get('.my-element').matchImage();
3927
*/
40-
matchImage(options?: Cypress.MatchImageOptions): Chainable<MatchImageReturn>;
28+
matchImage(options?: Cypress.MatchImageOptions): Chainable<Subject>;
4129
}
4230
}
4331
}
4432

33+
const nameCacheCounter: Record<string, number> = {};
34+
4535
const constructCypressError = (log: Cypress.Log, err: Error) => {
4636
// only way to throw & log the message properly in Cypress
4737
// https://github.com/cypress-io/cypress/blob/5f94cad3cb4126e0567290b957050c33e3a78e3c/packages/driver/src/cypress/error_utils.ts#L214-L216
@@ -50,77 +40,96 @@ const constructCypressError = (log: Cypress.Log, err: Error) => {
5040
return err;
5141
};
5242

53-
export const getConfig = (options: Cypress.MatchImageOptions) => ({
54-
scaleFactor:
55-
Cypress.env("pluginVisualRegressionForceDeviceScaleFactor") === false
56-
? 1
57-
: 1 / window.devicePixelRatio,
58-
updateImages:
59-
options.updateImages ||
60-
(Cypress.env("pluginVisualRegressionUpdateImages") as
61-
| boolean
62-
| undefined) ||
63-
false,
64-
imagesDir:
43+
const getImagesDir = (options: Cypress.MatchImageOptions) => {
44+
const imagesDir =
6545
options.imagesDir ||
66-
(Cypress.env("pluginVisualRegressionImagesDir") as string | undefined) ||
67-
"__image_snapshots__",
68-
maxDiffThreshold:
69-
options.maxDiffThreshold ||
70-
(Cypress.env("pluginVisualRegressionMaxDiffThreshold") as
71-
| number
72-
| undefined) ||
73-
0.01,
74-
diffConfig:
75-
options.diffConfig ||
76-
(Cypress.env("pluginVisualRegressionDiffConfig") as
77-
| Parameters<typeof pixelmatch>[5]
78-
| undefined) ||
79-
{},
80-
screenshotConfig:
81-
options.screenshotConfig ||
82-
(Cypress.env("pluginVisualRegressionScreenshotConfig") as
83-
| Partial<Cypress.ScreenshotDefaultsOptions>
84-
| undefined) ||
85-
{},
86-
matchAgainstPath: options.matchAgainstPath || undefined,
87-
});
46+
(Cypress.env("pluginVisualRegressionImagesDir") as string | undefined);
47+
48+
// TODO: remove in 4.0.0
49+
if (imagesDir) {
50+
console.warn(
51+
"@frsource/cypress-plugin-visual-regression-diff] `imagesDir` option is deprecated, use `imagesPath` instead (https://github.com/FRSOURCE/cypress-plugin-visual-regression-diff#configuration)"
52+
);
53+
}
54+
55+
return imagesDir;
56+
};
57+
58+
export const getConfig = (options: Cypress.MatchImageOptions) => {
59+
const imagesDir = getImagesDir(options);
60+
61+
return {
62+
scaleFactor:
63+
Cypress.env("pluginVisualRegressionForceDeviceScaleFactor") === false
64+
? 1
65+
: 1 / window.devicePixelRatio,
66+
updateImages:
67+
options.updateImages ||
68+
(Cypress.env("pluginVisualRegressionUpdateImages") as
69+
| boolean
70+
| undefined) ||
71+
false,
72+
imagesPath:
73+
(imagesDir && `{spec_path}/${imagesDir}`) ||
74+
options.imagesPath ||
75+
(Cypress.env("pluginVisualRegressionImagesPath") as string | undefined) ||
76+
"{spec_path}/__image_snapshots__",
77+
maxDiffThreshold:
78+
options.maxDiffThreshold ||
79+
(Cypress.env("pluginVisualRegressionMaxDiffThreshold") as
80+
| number
81+
| undefined) ||
82+
0.01,
83+
diffConfig:
84+
options.diffConfig ||
85+
(Cypress.env("pluginVisualRegressionDiffConfig") as
86+
| Parameters<typeof pixelmatch>[5]
87+
| undefined) ||
88+
{},
89+
screenshotConfig:
90+
options.screenshotConfig ||
91+
(Cypress.env("pluginVisualRegressionScreenshotConfig") as
92+
| Partial<Cypress.ScreenshotDefaultsOptions>
93+
| undefined) ||
94+
{},
95+
};
96+
};
8897

8998
Cypress.Commands.add(
9099
"matchImage",
91100
{ prevSubject: "optional" },
92101
(subject, options = {}) => {
93102
const $el = subject as JQuery<HTMLElement> | undefined;
94-
let title: string;
103+
let title = options.title || Cypress.currentTest.titlePath.join(" ");
104+
if (typeof nameCacheCounter[title] === "undefined")
105+
nameCacheCounter[title] = -1;
106+
title += ` #${++nameCacheCounter[title]}`;
95107

96108
const {
97109
scaleFactor,
98110
updateImages,
99-
imagesDir,
111+
imagesPath,
100112
maxDiffThreshold,
101113
diffConfig,
102114
screenshotConfig,
103-
matchAgainstPath,
104115
} = getConfig(options);
105116

106117
return cy
107118
.then(() =>
108-
cy.task<{ screenshotPath: string; title: string }>(
109-
TASK.getScreenshotPathInfo,
119+
cy.task<string>(
120+
TASK.getScreenshotPath,
110121
{
111-
titleFromOptions:
112-
options.title || Cypress.currentTest.titlePath.join(" "),
113-
imagesDir,
122+
title,
123+
imagesPath,
114124
specPath: Cypress.spec.relative,
115125
},
116126
{ log: false }
117127
)
118128
)
119-
.then(({ screenshotPath, title: titleFromTask }) => {
120-
title = titleFromTask;
129+
.then((screenshotPath) => {
121130
let imgPath: string;
122131
return ($el ? cy.wrap($el) : cy)
123-
.screenshot(screenshotPath as string, {
132+
.screenshot(screenshotPath, {
124133
...screenshotConfig,
125134
onAfterScreenshot(el, props) {
126135
imgPath = props.path;
@@ -130,14 +139,14 @@ Cypress.Commands.add(
130139
})
131140
.then(() => imgPath);
132141
})
133-
.then((imgPath) => {
134-
return cy
142+
.then((imgPath) =>
143+
cy
135144
.task<CompareImagesTaskReturn>(
136145
TASK.compareImages,
137146
{
138147
scaleFactor,
139148
imgNew: imgPath,
140-
imgOld: matchAgainstPath || imgPath.replace(FILE_SUFFIX.actual, ""),
149+
imgOld: imgPath.replace(FILE_SUFFIX.actual, ""),
141150
updateImages,
142151
maxDiffThreshold,
143152
diffConfig,
@@ -148,7 +157,7 @@ Cypress.Commands.add(
148157
res,
149158
imgPath,
150159
}))
151-
})
160+
)
152161
.then(({ res, imgPath }) => {
153162
const log = Cypress.log({
154163
name: "log",
@@ -185,19 +194,6 @@ Cypress.Commands.add(
185194
log.set("consoleProps", () => res);
186195
throw constructCypressError(log, new Error(res.message));
187196
}
188-
189-
return {
190-
diffValue: res.imgDiff,
191-
imgNewPath: imgPath,
192-
imgPath: imgPath.replace(FILE_SUFFIX.actual, ""),
193-
imgDiffPath: imgPath.replace(FILE_SUFFIX.actual, FILE_SUFFIX.diff),
194-
imgNewBase64: res.imgNewBase64,
195-
imgBase64: res.imgOldBase64,
196-
imgDiffBase64: res.imgDiffBase64,
197-
imgNew: typeof res.imgNewBase64 === 'string' ? Cypress.Buffer.from(res.imgNewBase64, 'base64') : undefined,
198-
img: typeof res.imgOldBase64 === 'string' ? Cypress.Buffer.from(res.imgOldBase64, 'base64') : undefined,
199-
imgDiff: typeof res.imgDiffBase64 === 'string' ? Cypress.Buffer.from(res.imgDiffBase64, 'base64') : undefined,
200-
}
201197
});
202198
}
203199
);

0 commit comments

Comments
 (0)