Skip to content

Commit 1ba38a6

Browse files
committed
feat: introduct 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 BREAKING CHANGE: deprecate imagesDir option in favor of imagesPath - see docs for additional information Signed-off-by: Jakub Freisler <[email protected]>
1 parent 566599f commit 1ba38a6

File tree

6 files changed

+132
-88
lines changed

6 files changed

+132
-88
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,13 @@ cy.matchImage({
165165
// default: false
166166
updateImages: true,
167167
// directory path in which screenshot images will be stored
168-
// image visualiser will normalise path separators depending on OS it's being run within, so always use / for nested paths
169-
// default: '__image_snapshots__'
170-
imagesDir: 'this-might-be-your-custom/maybe-nested-directory',
168+
// relative path are resolved against project root
169+
// absolute paths (both on unix and windows OS) supported
170+
// 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
171+
// There are one special variable available to be used in the path:
172+
// - {spec_path} - relative path leading from project root to the current spec file directory (e.g. `/src/components/my-tested-component`)
173+
// default: '{spec_path}/__image_snapshots__'
174+
imagesPath: 'this-might-be-your-custom/maybe-nested-directory',
171175
// maximum threshold above which the test should fail
172176
// default: 0.01
173177
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: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
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(`^${PATH_VARIABLES.winSystemRootPath}\\${path.sep}([A-Z])\\${path.sep}`);
9+
const MIMIC_ROOT_UNIX_REGEX = new RegExp(`^${PATH_VARIABLES.unixSystemRootPath}\\${path.sep}`);
10+
811
const getConfigVariableOrThrow = <K extends keyof Cypress.PluginConfigOptions>(
912
config: Cypress.PluginConfigOptions,
1013
name: K
@@ -14,60 +17,61 @@ const getConfigVariableOrThrow = <K extends keyof Cypress.PluginConfigOptions>(
1417
}
1518

1619
/* c8 ignore start */
17-
throw `[Image snapshot] CypressConfig.${name} cannot be missing or \`false\`!`;
20+
throw `[@frsource/cypress-plugin-visual-regression-diff] CypressConfig.${name} cannot be missing or \`false\`!`;
1821
};
1922
/* c8 ignore stop */
2023

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-
);
36-
};
24+
const parseAbsolutePath = ({
25+
screenshotPath, projectRoot,
26+
}: {
27+
screenshotPath: string; projectRoot: string;
28+
}) => {
29+
let newAbsolutePath: string;
30+
const matchedMimicingWinRoot = screenshotPath.match(MIMIC_ROOT_WIN_REGEX);
31+
const matchedMimicingUnixRoot = screenshotPath.match(MIMIC_ROOT_UNIX_REGEX);
32+
if (matchedMimicingWinRoot && matchedMimicingWinRoot[1]) {
33+
const driveLetter = matchedMimicingWinRoot[1];
34+
newAbsolutePath = path.join(`${driveLetter}:\\`, screenshotPath.substring(matchedMimicingWinRoot[0].length));
35+
} else if (matchedMimicingUnixRoot) {
36+
newAbsolutePath = path.sep + screenshotPath.substring(matchedMimicingUnixRoot[0].length);
37+
} else {
38+
newAbsolutePath = path.join(projectRoot, screenshotPath);
39+
}
40+
return path.normalize(newAbsolutePath);
41+
}
3742

3843
export const initAfterScreenshotHook =
3944
(config: Cypress.PluginConfigOptions) =>
4045
(
4146
details: Cypress.ScreenshotDetails
42-
):
43-
| void
47+
): void
4448
| Cypress.AfterScreenshotReturnObject
4549
| Promise<Cypress.AfterScreenshotReturnObject> => {
4650
// it's not a screenshot generated by FRSOURCE Cypress Plugin Visual Regression Diff
4751
/* c8 ignore start */
4852
if (details.name?.indexOf(IMAGE_SNAPSHOT_PREFIX) !== 0) return;
4953
/* c8 ignore stop */
50-
return new Promise((resolve, reject) => {
51-
const screenshotsFolder = getConfigVariableOrThrow(
52-
config,
53-
"screenshotsFolder"
54-
);
54+
const screenshotsFolder = getConfigVariableOrThrow(
55+
config,
56+
"screenshotsFolder"
57+
);
5558

56-
const newRelativePath = details.name.substring(
59+
return new Promise(async (resolve, reject) => {
60+
const screenshotPath = details.name.substring(
5761
IMAGE_SNAPSHOT_PREFIX.length + path.sep.length
5862
);
59-
const newAbsolutePath = path.normalize(
60-
path.join(config.projectRoot, newRelativePath)
61-
);
63+
const newAbsolutePath = parseAbsolutePath({ screenshotPath, projectRoot: config.projectRoot });
64+
65+
try {
66+
await moveFile(details.path, newAbsolutePath);
67+
await fs.rm(
68+
path.join(screenshotsFolder, IMAGE_SNAPSHOT_PREFIX),
69+
{ recursive: true, force: true },
70+
);
6271

63-
void moveFile(details.path, newAbsolutePath)
64-
.then(() =>
65-
removeScreenshotsDirectory(
66-
screenshotsFolder,
67-
() => resolve({ path: newAbsolutePath }),
68-
reject
69-
)
70-
)
71-
.catch(reject);
72+
resolve({ path: newAbsolutePath });
73+
} catch(e) {
74+
reject(e);
75+
}
7276
});
7377
};

src/commands.ts

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ 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;
1620
};
@@ -36,40 +40,50 @@ const constructCypressError = (log: Cypress.Log, err: Error) => {
3640
return err;
3741
};
3842

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

7488
Cypress.Commands.add(
7589
"matchImage",
@@ -84,19 +98,19 @@ Cypress.Commands.add(
8498
const {
8599
scaleFactor,
86100
updateImages,
87-
imagesDir,
101+
imagesPath,
88102
maxDiffThreshold,
89103
diffConfig,
90104
screenshotConfig,
91105
} = getConfig(options);
92106

93107
return cy
94108
.then(() =>
95-
cy.task(
109+
cy.task<string>(
96110
TASK.getScreenshotPath,
97111
{
98112
title,
99-
imagesDir,
113+
imagesPath,
100114
specPath: Cypress.spec.relative,
101115
},
102116
{ log: false }
@@ -105,7 +119,7 @@ Cypress.Commands.add(
105119
.then((screenshotPath) => {
106120
let imgPath: string;
107121
return ($el ? cy.wrap($el) : cy)
108-
.screenshot(screenshotPath as string, {
122+
.screenshot(screenshotPath, {
109123
...screenshotConfig,
110124
onAfterScreenshot(el, props) {
111125
imgPath = props.path;

src/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ export const TASK = {
1515
doesFileExist: `${PLUGIN_NAME}-doesFileExist`,
1616
/* c8 ignore next */
1717
};
18+
19+
export const PATH_VARIABLES = {
20+
specPath: '{spec_path}',
21+
unixSystemRootPath: '{unix_system_root_path}',
22+
winSystemRootPath: '{win_system_root_path}',
23+
};
24+
25+
export const WINDOWS_LIKE_DRIVE_REGEX = /^[A-Z]:$/;

src/task.hook.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { PNG } from "pngjs";
44
import pixelmatch, { PixelmatchOptions } from "pixelmatch";
55
import moveFile from "move-file";
66
import sanitize from "sanitize-filename";
7-
import { FILE_SUFFIX, IMAGE_SNAPSHOT_PREFIX, TASK } from "./constants";
7+
import { FILE_SUFFIX, IMAGE_SNAPSHOT_PREFIX, TASK, PATH_VARIABLES, WINDOWS_LIKE_DRIVE_REGEX } from "./constants";
88
import { alignImagesToSameSize, importAndScaleImage } from "./image.utils";
99
import type { CompareImagesTaskReturn } from "./types";
1010

@@ -27,19 +27,33 @@ const moveSyncSafe = (pathFrom: string, pathTo: string) =>
2727

2828
export const getScreenshotPathTask = ({
2929
title,
30-
imagesDir,
30+
imagesPath,
3131
specPath,
3232
}: {
3333
title: string;
34-
imagesDir: string;
34+
imagesPath: string;
3535
specPath: string;
36-
}) =>
37-
path.join(
36+
}) => {
37+
const parsePathPartVariables = (pathPart: string, i: number) => {
38+
if (pathPart === PATH_VARIABLES.specPath) {
39+
return path.dirname(specPath);
40+
} else if (i === 0 && !pathPart) {
41+
// when unix-like absolute path
42+
return PATH_VARIABLES.unixSystemRootPath;
43+
} else if (i === 0 && WINDOWS_LIKE_DRIVE_REGEX.test(pathPart)) {
44+
// when win-like absolute path
45+
return path.join(PATH_VARIABLES.winSystemRootPath, pathPart[0]);
46+
}
47+
48+
return pathPart;
49+
};
50+
51+
return path.join(
3852
IMAGE_SNAPSHOT_PREFIX,
39-
path.dirname(specPath),
40-
...imagesDir.split("/"),
53+
...imagesPath.split("/").map(parsePathPartVariables),
4154
`${sanitize(title)}${FILE_SUFFIX.actual}.png`
4255
);
56+
}
4357

4458
export const approveImageTask = ({ img }: { img: string }) => {
4559
const oldImg = img.replace(FILE_SUFFIX.actual, "");

0 commit comments

Comments
 (0)