Skip to content

feat: introduce imagesPath option #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,13 @@ cy.matchImage({
// default: false
updateImages: true,
// directory path in which screenshot images will be stored
// image visualiser will normalise path separators depending on OS it's being run within, so always use / for nested paths
// default: '__image_snapshots__'
imagesDir: 'this-might-be-your-custom/maybe-nested-directory',
// relative path are resolved against project root
// absolute paths (both on unix and windows OS) supported
// 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
// There are one special variable available to be used in the path:
// - {spec_path} - relative path leading from project root to the current spec file directory (e.g. `/src/components/my-tested-component`)
// default: '{spec_path}/__image_snapshots__'
imagesPath: 'this-might-be-your-custom/maybe-nested-directory',
// maximum threshold above which the test should fail
// default: 0.01
maxDiffThreshold: 0.1,
Expand Down
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:open": "vue-cli-service test:e2e --env \"pluginVisualRegressionImagesDir=__image_snapshots_local__\"",
"test:open": "vue-cli-service test:e2e --env \"pluginVisualRegressionImagesPath={spec_path}/__image_snapshots_local__\"",
"test:run": "vue-cli-service test:e2e",
"test:ct": "yarn test:open --component",
"test:ct:ci": "yarn test:run --component --headless",
Expand Down
24 changes: 22 additions & 2 deletions src/afterScreenshot.hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { it, expect, describe } from "vitest";
import path from "path";
import { promises as fs, existsSync } from "fs";
import { initAfterScreenshotHook } from "./afterScreenshot.hook";
import { initAfterScreenshotHook, parseAbsolutePath } from "./afterScreenshot.hook";
import { dir, file, setGracefulCleanup } from "tmp-promise";
import { IMAGE_SNAPSHOT_PREFIX } from "./constants";
import { IMAGE_SNAPSHOT_PREFIX, PATH_VARIABLES } from "./constants";

setGracefulCleanup();

Expand Down Expand Up @@ -31,3 +31,23 @@ describe("initAfterScreenshotHook", () => {
await fs.unlink(expectedNewPath);
});
});

describe('parseAbsolutePath', () => {
const projectRoot = '/its/project/root';

it('resolves relative paths against project root', () => {
expect(parseAbsolutePath({ screenshotPath: 'some/path.png', projectRoot }))
.toBe('/its/project/root/some/path.png');
});

it('builds proper win paths when found', () => {
expect(parseAbsolutePath({ screenshotPath: `${PATH_VARIABLES.winSystemRootPath}/D/some/path.png`, projectRoot }))
// that's expected output accorind to https://stackoverflow.com/a/64135721/8805801
.toBe('D:\\/some/path.png');
});

it('resolves relative paths against project root', () => {
expect(parseAbsolutePath({ screenshotPath: `${PATH_VARIABLES.unixSystemRootPath}/some/path.png`, projectRoot }))
.toBe('/some/path.png');
});
});
91 changes: 52 additions & 39 deletions src/afterScreenshot.hook.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import path from "path";
import fs from "fs";
import { promises as fs } from "fs";
import moveFile from "move-file";
import { IMAGE_SNAPSHOT_PREFIX } from "./constants";
import { IMAGE_SNAPSHOT_PREFIX, PATH_VARIABLES } from "./constants";

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

const MIMIC_ROOT_WIN_REGEX = new RegExp(
`^${PATH_VARIABLES.winSystemRootPath}\\${path.sep}([A-Z])\\${path.sep}`
);
const MIMIC_ROOT_UNIX_REGEX = new RegExp(
`^${PATH_VARIABLES.unixSystemRootPath}\\${path.sep}`
);

const getConfigVariableOrThrow = <K extends keyof Cypress.PluginConfigOptions>(
config: Cypress.PluginConfigOptions,
name: K
Expand All @@ -14,25 +21,33 @@ const getConfigVariableOrThrow = <K extends keyof Cypress.PluginConfigOptions>(
}

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

const removeScreenshotsDirectory = (
screenshotsFolder: string,
onSuccess: () => void,
onError: (e: Error) => void
) => {
fs.rm(
path.join(screenshotsFolder, IMAGE_SNAPSHOT_PREFIX),
{ recursive: true, force: true },
(err) => {
/* c8 ignore start */
if (err) return onError(err);
/* c8 ignore stop */
onSuccess();
}
);
export const parseAbsolutePath = ({
screenshotPath,
projectRoot,
}: {
screenshotPath: string;
projectRoot: string;
}) => {
let newAbsolutePath: string;
const matchedMimicingWinRoot = screenshotPath.match(MIMIC_ROOT_WIN_REGEX);
const matchedMimicingUnixRoot = screenshotPath.match(MIMIC_ROOT_UNIX_REGEX);
if (matchedMimicingWinRoot && matchedMimicingWinRoot[1]) {
const driveLetter = matchedMimicingWinRoot[1];
newAbsolutePath = path.join(
`${driveLetter}:\\`,
screenshotPath.substring(matchedMimicingWinRoot[0].length)
);
} else if (matchedMimicingUnixRoot) {
newAbsolutePath =
path.sep + screenshotPath.substring(matchedMimicingUnixRoot[0].length);
} else {
newAbsolutePath = path.join(projectRoot, screenshotPath);
}
return path.normalize(newAbsolutePath);
};

export const initAfterScreenshotHook =
Expand All @@ -47,27 +62,25 @@ export const initAfterScreenshotHook =
/* c8 ignore start */
if (details.name?.indexOf(IMAGE_SNAPSHOT_PREFIX) !== 0) return;
/* c8 ignore stop */
return new Promise((resolve, reject) => {
const screenshotsFolder = getConfigVariableOrThrow(
config,
"screenshotsFolder"
);
const screenshotsFolder = getConfigVariableOrThrow(
config,
"screenshotsFolder"
);
const screenshotPath = details.name.substring(
IMAGE_SNAPSHOT_PREFIX.length + path.sep.length
);
const newAbsolutePath = parseAbsolutePath({
screenshotPath,
projectRoot: config.projectRoot,
});

const newRelativePath = details.name.substring(
IMAGE_SNAPSHOT_PREFIX.length + path.sep.length
);
const newAbsolutePath = path.normalize(
path.join(config.projectRoot, newRelativePath)
);
return (async () => {
await moveFile(details.path, newAbsolutePath);
await fs.rm(path.join(screenshotsFolder, IMAGE_SNAPSHOT_PREFIX), {
recursive: true,
force: true,
});

void moveFile(details.path, newAbsolutePath)
.then(() =>
removeScreenshotsDirectory(
screenshotsFolder,
() => resolve({ path: newAbsolutePath }),
reject
)
)
.catch(reject);
});
return { path: newAbsolutePath };
})();
};
137 changes: 65 additions & 72 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,13 @@ declare global {
screenshotConfig?: Partial<Cypress.ScreenshotDefaultsOptions>;
diffConfig?: Parameters<typeof pixelmatch>[5];
updateImages?: boolean;
/**
* @deprecated since version 3.0, use imagesPath instead
*/
imagesDir?: string;
imagesPath?: string;
maxDiffThreshold?: number;
title?: string;
matchAgainstPath?: string;
// IDEA: to be implemented if support for files NOT from filesystem needed
// matchAgainst?: string | Buffer;
};

type MatchImageReturn = {
diffValue: number | undefined;
imgNewPath: string;
imgPath: string;
imgDiffPath: string;
imgNewBase64: string | undefined;
imgBase64: string | undefined;
imgDiffBase64: string | undefined;
imgNew: InstanceType<Cypress['Buffer']> | undefined;
img: InstanceType<Cypress['Buffer']> | undefined;
imgDiff: InstanceType<Cypress['Buffer']> | undefined;
};

interface Chainable<Subject> {
Expand All @@ -37,7 +25,7 @@ declare global {
* @memberof Cypress.Chainable
* @example cy.get('.my-element').matchImage();
*/
matchImage(options?: Cypress.MatchImageOptions): Chainable<MatchImageReturn>;
matchImage(options?: Cypress.MatchImageOptions): Chainable<Subject>;
}
}
}
Expand All @@ -50,41 +38,60 @@ const constructCypressError = (log: Cypress.Log, err: Error) => {
return err;
};

export const getConfig = (options: Cypress.MatchImageOptions) => ({
scaleFactor:
Cypress.env("pluginVisualRegressionForceDeviceScaleFactor") === false
? 1
: 1 / window.devicePixelRatio,
updateImages:
options.updateImages ||
(Cypress.env("pluginVisualRegressionUpdateImages") as
| boolean
| undefined) ||
false,
imagesDir:
const getImagesDir = (options: Cypress.MatchImageOptions) => {
const imagesDir =
options.imagesDir ||
(Cypress.env("pluginVisualRegressionImagesDir") as string | undefined) ||
"__image_snapshots__",
maxDiffThreshold:
options.maxDiffThreshold ||
(Cypress.env("pluginVisualRegressionMaxDiffThreshold") as
| number
| undefined) ||
0.01,
diffConfig:
options.diffConfig ||
(Cypress.env("pluginVisualRegressionDiffConfig") as
| Parameters<typeof pixelmatch>[5]
| undefined) ||
{},
screenshotConfig:
options.screenshotConfig ||
(Cypress.env("pluginVisualRegressionScreenshotConfig") as
| Partial<Cypress.ScreenshotDefaultsOptions>
| undefined) ||
{},
matchAgainstPath: options.matchAgainstPath || undefined,
});
(Cypress.env("pluginVisualRegressionImagesDir") as string | undefined);

// TODO: remove in 4.0.0
if (imagesDir) {
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)"
);
}

return imagesDir;
};

export const getConfig = (options: Cypress.MatchImageOptions) => {
const imagesDir = getImagesDir(options);

return {
scaleFactor:
Cypress.env("pluginVisualRegressionForceDeviceScaleFactor") === false
? 1
: 1 / window.devicePixelRatio,
updateImages:
options.updateImages ||
(Cypress.env("pluginVisualRegressionUpdateImages") as
| boolean
| undefined) ||
false,
imagesPath:
(imagesDir && `{spec_path}/${imagesDir}`) ||
options.imagesPath ||
(Cypress.env("pluginVisualRegressionImagesPath") as string | undefined) ||
"{spec_path}/__image_snapshots__",
maxDiffThreshold:
options.maxDiffThreshold ||
(Cypress.env("pluginVisualRegressionMaxDiffThreshold") as
| number
| undefined) ||
0.01,
diffConfig:
options.diffConfig ||
(Cypress.env("pluginVisualRegressionDiffConfig") as
| Parameters<typeof pixelmatch>[5]
| undefined) ||
{},
screenshotConfig:
options.screenshotConfig ||
(Cypress.env("pluginVisualRegressionScreenshotConfig") as
| Partial<Cypress.ScreenshotDefaultsOptions>
| undefined) ||
{},
};
};

Cypress.Commands.add(
"matchImage",
Expand All @@ -96,11 +103,10 @@ Cypress.Commands.add(
const {
scaleFactor,
updateImages,
imagesDir,
imagesPath,
maxDiffThreshold,
diffConfig,
screenshotConfig,
matchAgainstPath,
} = getConfig(options);

return cy
Expand All @@ -110,7 +116,7 @@ Cypress.Commands.add(
{
titleFromOptions:
options.title || Cypress.currentTest.titlePath.join(" "),
imagesDir,
imagesPath,
specPath: Cypress.spec.relative,
},
{ log: false }
Expand All @@ -120,7 +126,7 @@ Cypress.Commands.add(
title = titleFromTask;
let imgPath: string;
return ($el ? cy.wrap($el) : cy)
.screenshot(screenshotPath as string, {
.screenshot(screenshotPath, {
...screenshotConfig,
onAfterScreenshot(el, props) {
imgPath = props.path;
Expand All @@ -130,14 +136,14 @@ Cypress.Commands.add(
})
.then(() => imgPath);
})
.then((imgPath) => {
return cy
.then((imgPath) =>
cy
.task<CompareImagesTaskReturn>(
TASK.compareImages,
{
scaleFactor,
imgNew: imgPath,
imgOld: matchAgainstPath || imgPath.replace(FILE_SUFFIX.actual, ""),
imgOld: imgPath.replace(FILE_SUFFIX.actual, ""),
updateImages,
maxDiffThreshold,
diffConfig,
Expand All @@ -148,7 +154,7 @@ Cypress.Commands.add(
res,
imgPath,
}))
})
)
.then(({ res, imgPath }) => {
const log = Cypress.log({
name: "log",
Expand Down Expand Up @@ -185,19 +191,6 @@ Cypress.Commands.add(
log.set("consoleProps", () => res);
throw constructCypressError(log, new Error(res.message));
}

return {
diffValue: res.imgDiff,
imgNewPath: imgPath,
imgPath: imgPath.replace(FILE_SUFFIX.actual, ""),
imgDiffPath: imgPath.replace(FILE_SUFFIX.actual, FILE_SUFFIX.diff),
imgNewBase64: res.imgNewBase64,
imgBase64: res.imgOldBase64,
imgDiffBase64: res.imgDiffBase64,
imgNew: typeof res.imgNewBase64 === 'string' ? Cypress.Buffer.from(res.imgNewBase64, 'base64') : undefined,
img: typeof res.imgOldBase64 === 'string' ? Cypress.Buffer.from(res.imgOldBase64, 'base64') : undefined,
imgDiff: typeof res.imgDiffBase64 === 'string' ? Cypress.Buffer.from(res.imgDiffBase64, 'base64') : undefined,
}
});
}
);
Loading