Skip to content

Commit b358022

Browse files
authored
feat: introduce imagesPath option (#152)
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 references issue #147 BREAKING CHANGE: deprecate imagesDir option in favor of imagesPath - see docs for additional information
1 parent 38679a7 commit b358022

File tree

9 files changed

+225
-135
lines changed

9 files changed

+225
-135
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_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: 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+
export 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: 65 additions & 72 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,7 +25,7 @@ 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
}
@@ -50,41 +38,60 @@ const constructCypressError = (log: Cypress.Log, err: Error) => {
5038
return err;
5139
};
5240

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

8996
Cypress.Commands.add(
9097
"matchImage",
@@ -96,11 +103,10 @@ Cypress.Commands.add(
96103
const {
97104
scaleFactor,
98105
updateImages,
99-
imagesDir,
106+
imagesPath,
100107
maxDiffThreshold,
101108
diffConfig,
102109
screenshotConfig,
103-
matchAgainstPath,
104110
} = getConfig(options);
105111

106112
return cy
@@ -110,7 +116,7 @@ Cypress.Commands.add(
110116
{
111117
titleFromOptions:
112118
options.title || Cypress.currentTest.titlePath.join(" "),
113-
imagesDir,
119+
imagesPath,
114120
specPath: Cypress.spec.relative,
115121
},
116122
{ log: false }
@@ -120,7 +126,7 @@ Cypress.Commands.add(
120126
title = titleFromTask;
121127
let imgPath: string;
122128
return ($el ? cy.wrap($el) : cy)
123-
.screenshot(screenshotPath as string, {
129+
.screenshot(screenshotPath, {
124130
...screenshotConfig,
125131
onAfterScreenshot(el, props) {
126132
imgPath = props.path;
@@ -130,14 +136,14 @@ Cypress.Commands.add(
130136
})
131137
.then(() => imgPath);
132138
})
133-
.then((imgPath) => {
134-
return cy
139+
.then((imgPath) =>
140+
cy
135141
.task<CompareImagesTaskReturn>(
136142
TASK.compareImages,
137143
{
138144
scaleFactor,
139145
imgNew: imgPath,
140-
imgOld: matchAgainstPath || imgPath.replace(FILE_SUFFIX.actual, ""),
146+
imgOld: imgPath.replace(FILE_SUFFIX.actual, ""),
141147
updateImages,
142148
maxDiffThreshold,
143149
diffConfig,
@@ -148,7 +154,7 @@ Cypress.Commands.add(
148154
res,
149155
imgPath,
150156
}))
151-
})
157+
)
152158
.then(({ res, imgPath }) => {
153159
const log = Cypress.log({
154160
name: "log",
@@ -185,19 +191,6 @@ Cypress.Commands.add(
185191
log.set("consoleProps", () => res);
186192
throw constructCypressError(log, new Error(res.message));
187193
}
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-
}
201194
});
202195
}
203196
);

0 commit comments

Comments
 (0)