Skip to content

Commit 40e0cec

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 566599f commit 40e0cec

File tree

6 files changed

+156
-87
lines changed

6 files changed

+156
-87
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: 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: 61 additions & 37 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,60 @@ 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:
43+
const getImagesDir = (options: Cypress.MatchImageOptions) => {
44+
const imagesDir =
5145
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-
});
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+
};
7397

7498
Cypress.Commands.add(
7599
"matchImage",
@@ -84,19 +108,19 @@ Cypress.Commands.add(
84108
const {
85109
scaleFactor,
86110
updateImages,
87-
imagesDir,
111+
imagesPath,
88112
maxDiffThreshold,
89113
diffConfig,
90114
screenshotConfig,
91115
} = getConfig(options);
92116

93117
return cy
94118
.then(() =>
95-
cy.task(
119+
cy.task<string>(
96120
TASK.getScreenshotPath,
97121
{
98122
title,
99-
imagesDir,
123+
imagesPath,
100124
specPath: Cypress.spec.relative,
101125
},
102126
{ log: false }
@@ -105,7 +129,7 @@ Cypress.Commands.add(
105129
.then((screenshotPath) => {
106130
let imgPath: string;
107131
return ($el ? cy.wrap($el) : cy)
108-
.screenshot(screenshotPath as string, {
132+
.screenshot(screenshotPath, {
109133
...screenshotConfig,
110134
onAfterScreenshot(el, props) {
111135
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: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ 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 {
8+
FILE_SUFFIX,
9+
IMAGE_SNAPSHOT_PREFIX,
10+
TASK,
11+
PATH_VARIABLES,
12+
WINDOWS_LIKE_DRIVE_REGEX,
13+
} from "./constants";
814
import { alignImagesToSameSize, importAndScaleImage } from "./image.utils";
915
import type { CompareImagesTaskReturn } from "./types";
1016

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

2834
export const getScreenshotPathTask = ({
2935
title,
30-
imagesDir,
36+
imagesPath,
3137
specPath,
3238
}: {
3339
title: string;
34-
imagesDir: string;
40+
imagesPath: string;
3541
specPath: string;
36-
}) =>
37-
path.join(
42+
}) => {
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+
}
53+
54+
return pathPart;
55+
};
56+
57+
return path.join(
3858
IMAGE_SNAPSHOT_PREFIX,
39-
path.dirname(specPath),
40-
...imagesDir.split("/"),
59+
...imagesPath.split("/").map(parsePathPartVariables),
4160
`${sanitize(title)}${FILE_SUFFIX.actual}.png`
4261
);
62+
};
4363

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

0 commit comments

Comments
 (0)