Skip to content

Commit 38679a7

Browse files
authored
feat: auto clean unused files (FRSOURCE#124)
add metadata to images generated by the plugin recreate fixture screenshots (add metadata) add documentation info closes FRSOURCE#118 BREAKING CHANGE: To use autocleanup feature you need to update all of the screenshots, best do it by running your test suite with cypress env 'pluginVisualRegressionUpdateImages' set to true.
1 parent 7a5e3a8 commit 38679a7

17 files changed

+292
-75
lines changed

README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,16 @@ cy.matchImage();
141141

142142
## Example
143143

144-
Still got troubles with installation? Have a look at [example directory of this repo](./example) to see how this plugin can be used in e2e or component-testing Cypress within your project.
144+
Still got troubles with installation? Have a look at [example directory of this repo](./example) to see how this plugin can be used in e2e or component-testing Cypress environment within your project.
145+
146+
## Automatic clean up of unused images
147+
148+
It's useful to remove screenshots generated by the visual regression plugin that are not used by any test anymore.
149+
Enable this feature via env variable and enjoy freed up storage space 🚀:
150+
151+
```bash
152+
npx cypress run --env "pluginVisualRegressionCleanupUnusedImages=true"
153+
```
145154

146155
## Configuration
147156

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
describe('Cleanup test', () => {
2+
it('Create screenshot to be removed', () => {
3+
cy.visit('/');
4+
cy.get('[data-testid="description"]').matchImage();
5+
});
6+
});
34 Bytes
Loading
34 Bytes
Loading

__tests__/fixtures/screenshot.png

34 Bytes
Loading

example/yarn.lock

+10-1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ __metadata:
145145
resolution: "@frsource/cypress-plugin-visual-regression-diff@portal:..::locator=example%40workspace%3A."
146146
dependencies:
147147
"@frsource/base64": 1.0.3
148+
glob: ^8.0.3
149+
meta-png: ^1.0.3
148150
move-file: 2.1.0
149151
pixelmatch: 5.3.0
150152
pngjs: 6.0.0
@@ -3380,7 +3382,7 @@ __metadata:
33803382
languageName: node
33813383
linkType: hard
33823384

3383-
"glob@npm:^8.0.1":
3385+
"glob@npm:^8.0.1, glob@npm:^8.0.3":
33843386
version: 8.0.3
33853387
resolution: "glob@npm:8.0.3"
33863388
dependencies:
@@ -4502,6 +4504,13 @@ __metadata:
45024504
languageName: node
45034505
linkType: hard
45044506

4507+
"meta-png@npm:^1.0.3":
4508+
version: 1.0.3
4509+
resolution: "meta-png@npm:1.0.3"
4510+
checksum: cc7e1e0950b149273eb127622d8079725855ca14fb5e0175a4f1a7946d7f4a1c92e78de9f44eb1b9fa339c60f43b099c5135dc06b218cf77879fbd0a7f6ecddb
4511+
languageName: node
4512+
linkType: hard
4513+
45054514
"methods@npm:~1.1.2":
45064515
version: 1.1.2
45074516
resolution: "methods@npm:1.1.2"

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"@semantic-release/git": "10.0.1",
6969
"@semantic-release/npm": "9.0.1",
7070
"@semantic-release/release-notes-generator": "10.0.3",
71+
"@types/glob": "^8.0.0",
7172
"@types/pixelmatch": "5.2.4",
7273
"@types/pngjs": "6.0.1",
7374
"@types/sharp": "0.31.0",
@@ -106,6 +107,8 @@
106107
],
107108
"dependencies": {
108109
"@frsource/base64": "1.0.3",
110+
"glob": "^8.0.3",
111+
"meta-png": "^1.0.3",
109112
"move-file": "2.1.0",
110113
"pixelmatch": "5.3.0",
111114
"pngjs": "6.0.0",

src/commands.ts

+7-10
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ declare global {
4242
}
4343
}
4444

45-
const nameCacheCounter: Record<string, number> = {};
46-
4745
const constructCypressError = (log: Cypress.Log, err: Error) => {
4846
// only way to throw & log the message properly in Cypress
4947
// https://github.com/cypress-io/cypress/blob/5f94cad3cb4126e0567290b957050c33e3a78e3c/packages/driver/src/cypress/error_utils.ts#L214-L216
@@ -93,10 +91,7 @@ Cypress.Commands.add(
9391
{ prevSubject: "optional" },
9492
(subject, options = {}) => {
9593
const $el = subject as JQuery<HTMLElement> | undefined;
96-
let title = options.title || Cypress.currentTest.titlePath.join(" ");
97-
if (typeof nameCacheCounter[title] === "undefined")
98-
nameCacheCounter[title] = -1;
99-
title += ` #${++nameCacheCounter[title]}`;
94+
let title: string;
10095

10196
const {
10297
scaleFactor,
@@ -110,17 +105,19 @@ Cypress.Commands.add(
110105

111106
return cy
112107
.then(() =>
113-
cy.task(
114-
TASK.getScreenshotPath,
108+
cy.task<{ screenshotPath: string; title: string }>(
109+
TASK.getScreenshotPathInfo,
115110
{
116-
title,
111+
titleFromOptions:
112+
options.title || Cypress.currentTest.titlePath.join(" "),
117113
imagesDir,
118114
specPath: Cypress.spec.relative,
119115
},
120116
{ log: false }
121117
)
122118
)
123-
.then((screenshotPath) => {
119+
.then(({ screenshotPath, title: titleFromTask }) => {
120+
title = titleFromTask;
124121
let imgPath: string;
125122
return ($el ? cy.wrap($el) : cy)
126123
.screenshot(screenshotPath as string, {

src/constants.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* c8 ignore start */
12
const PLUGIN_NAME = "cp-visual-regression-diff";
23
export const LINK_PREFIX = `#${PLUGIN_NAME}-`;
34
export const OVERLAY_CLASS = `${PLUGIN_NAME}-overlay`;
@@ -9,9 +10,13 @@ export enum FILE_SUFFIX {
910
}
1011

1112
export const TASK = {
12-
getScreenshotPath: `${PLUGIN_NAME}-getScreenshotPath`,
13+
getScreenshotPathInfo: `${PLUGIN_NAME}-getScreenshotPathInfo`,
1314
compareImages: `${PLUGIN_NAME}-compareImages`,
1415
approveImage: `${PLUGIN_NAME}-approveImage`,
16+
cleanupImages: `${PLUGIN_NAME}-cleanupImages`,
1517
doesFileExist: `${PLUGIN_NAME}-doesFileExist`,
1618
/* c8 ignore next */
1719
};
20+
21+
export const METADATA_KEY = "FRSOURCE_CPVRD_V";
22+
/* c8 ignore stop */

src/image.utils.ts

+49-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
1+
import path from "path";
12
import fs from "fs";
23
import { PNG, PNGWithMetadata } from "pngjs";
34
import sharp from "sharp";
5+
import { addMetadata, getMetadata } from "meta-png";
6+
import glob from "glob";
7+
import { version } from "../package.json";
8+
import { wasScreenshotUsed } from "./screenshotPath.utils";
9+
import { METADATA_KEY } from "./constants";
10+
11+
export const addPNGMetadata = (png: Buffer) =>
12+
addMetadata(png, METADATA_KEY, version /* c8 ignore next */);
13+
export const getPNGMetadata = (png: Buffer) =>
14+
getMetadata(png, METADATA_KEY /* c8 ignore next */);
15+
export const isImageCurrentVersion = (png: Buffer) =>
16+
getPNGMetadata(png) === version;
17+
export const isImageGeneratedByPlugin = (png: Buffer) =>
18+
!!getPNGMetadata(png /* c8 ignore next */);
19+
20+
export const writePNG = (name: string, png: PNG | Buffer) =>
21+
fs.writeFileSync(
22+
name,
23+
addPNGMetadata(png instanceof PNG ? PNG.sync.write(png) : png)
24+
);
425

526
const inArea = (x: number, y: number, height: number, width: number) =>
627
y > height || x > width;
@@ -33,19 +54,22 @@ export const createImageResizer =
3354
/* c8 ignore next */
3455
};
3556

36-
export const importAndScaleImage = async (cfg: {
57+
export const scaleImageAndWrite = async ({
58+
scaleFactor,
59+
path,
60+
}: {
3761
scaleFactor: number;
3862
path: string;
3963
}) => {
40-
const imgBuffer = fs.readFileSync(cfg.path);
41-
const rawImgNew = PNG.sync.read(imgBuffer);
42-
if (cfg.scaleFactor === 1) return rawImgNew;
64+
const imgBuffer = fs.readFileSync(path);
65+
if (scaleFactor === 1) return imgBuffer;
4366

44-
const newImageWidth = Math.ceil(rawImgNew.width * cfg.scaleFactor);
45-
const newImageHeight = Math.ceil(rawImgNew.height * cfg.scaleFactor);
46-
await sharp(imgBuffer).resize(newImageWidth, newImageHeight).toFile(cfg.path);
67+
const rawImgNew = PNG.sync.read(imgBuffer);
68+
const newImageWidth = Math.ceil(rawImgNew.width * scaleFactor);
69+
const newImageHeight = Math.ceil(rawImgNew.height * scaleFactor);
70+
await sharp(imgBuffer).resize(newImageWidth, newImageHeight).toFile(path);
4771

48-
return PNG.sync.read(fs.readFileSync(cfg.path));
72+
return fs.readFileSync(path);
4973
};
5074

5175
export const alignImagesToSameSize = (
@@ -70,3 +94,20 @@ export const alignImagesToSameSize = (
7094
fillSizeDifference(resizedSecond, secondImageWidth, secondImageHeight),
7195
];
7296
};
97+
98+
export const cleanupUnused = (rootPath: string) => {
99+
glob
100+
.sync("**/*.png", {
101+
cwd: rootPath,
102+
ignore: "node_modules/**/*",
103+
})
104+
.forEach((pngPath) => {
105+
const absolutePath = path.join(rootPath, pngPath);
106+
if (
107+
!wasScreenshotUsed(pngPath) &&
108+
isImageGeneratedByPlugin(fs.readFileSync(absolutePath))
109+
) {
110+
fs.unlinkSync(absolutePath);
111+
}
112+
});
113+
};

src/plugins.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ export const initPlugin = (
2727
initForceDeviceScaleFactor(on);
2828
}
2929
/* c8 ignore stop */
30-
on("task", initTaskHook());
30+
on("task", initTaskHook(config));
3131
on("after:screenshot", initAfterScreenshotHook(config));
3232
};

src/screenshotPath.utils.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import path from "path";
2+
import { FILE_SUFFIX, IMAGE_SNAPSHOT_PREFIX } from "./constants";
3+
import sanitize from "sanitize-filename";
4+
5+
const nameCacheCounter: Record<string, number> = {};
6+
7+
export const generateScreenshotPath = ({
8+
titleFromOptions,
9+
imagesDir,
10+
specPath,
11+
}: {
12+
titleFromOptions: string;
13+
imagesDir: string;
14+
specPath: string;
15+
}) => {
16+
const screenshotPath = path.join(
17+
path.dirname(specPath),
18+
...imagesDir.split("/"),
19+
sanitize(titleFromOptions)
20+
);
21+
22+
if (typeof nameCacheCounter[screenshotPath] === "undefined") {
23+
nameCacheCounter[screenshotPath] = -1;
24+
}
25+
return path.join(
26+
IMAGE_SNAPSHOT_PREFIX,
27+
`${screenshotPath} #${++nameCacheCounter[screenshotPath]}${
28+
FILE_SUFFIX.actual
29+
}.png`
30+
);
31+
};
32+
33+
const screenshotPathRegex = new RegExp(
34+
`^([\\s\\S]+?) #([0-9]+)(?:(?:\\${FILE_SUFFIX.diff})|(?:\\${FILE_SUFFIX.actual}))?\\.(?:png|PNG)$`
35+
);
36+
export const wasScreenshotUsed = (imagePath: string) => {
37+
const matched = imagePath.match(screenshotPathRegex);
38+
/* c8 ignore next */ if (!matched) return false;
39+
const [, screenshotPath, numString] = matched;
40+
const num = parseInt(numString);
41+
/* c8 ignore next */ if (!screenshotPath || isNaN(num)) return false;
42+
return (
43+
screenshotPath in nameCacheCounter &&
44+
num <= nameCacheCounter[screenshotPath]
45+
);
46+
};

src/support.ts

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ before(() => {
7777
after(() => {
7878
if (!top) return null;
7979

80+
cy.task(TASK.cleanupImages, { log: false });
81+
8082
Cypress.$(top.document.body).on(
8183
"click",
8284
`a[href^="${LINK_PREFIX}"]`,

0 commit comments

Comments
 (0)