Skip to content

Commit bad6e34

Browse files
committed
feat: auto clean unused files
add documentation info add metadata to images generated by the plugin update fixtures screenshots (add metadata) closes #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. Signed-off-by: Jakub Freisler <[email protected]>
1 parent 6d3b58a commit bad6e34

16 files changed

+277
-74
lines changed

README.md

Lines changed: 10 additions & 1 deletion
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

Lines changed: 6 additions & 0 deletions
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

package.json

Lines changed: 3 additions & 0 deletions
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

Lines changed: 7 additions & 10 deletions
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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ export enum FILE_SUFFIX {
99
}
1010

1111
export const TASK = {
12-
getScreenshotPath: `${PLUGIN_NAME}-getScreenshotPath`,
12+
getScreenshotPathInfo: `${PLUGIN_NAME}-getScreenshotPathInfo`,
1313
compareImages: `${PLUGIN_NAME}-compareImages`,
1414
approveImage: `${PLUGIN_NAME}-approveImage`,
15+
cleanupImages: `${PLUGIN_NAME}-cleanupImages`,
1516
doesFileExist: `${PLUGIN_NAME}-doesFileExist`,
1617
/* c8 ignore next */
1718
};
19+
20+
export const METADATA_KEY = "FRSOURCE_CPVRD_V";

src/image.utils.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
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);
13+
export const getPNGMetadata = (png: Buffer) => getMetadata(png, METADATA_KEY);
14+
export const isImageCurrentVersion = (png: Buffer) =>
15+
getPNGMetadata(png) === version;
16+
export const isImageGeneratedByPlugin = (png: Buffer) => !!getPNGMetadata(png);
17+
18+
export const writePNG = (name: string, png: PNG | Buffer) =>
19+
fs.writeFileSync(
20+
name,
21+
addPNGMetadata(png instanceof PNG ? PNG.sync.write(png) : png)
22+
);
423

524
const inArea = (x: number, y: number, height: number, width: number) =>
625
y > height || x > width;
@@ -33,19 +52,22 @@ export const createImageResizer =
3352
/* c8 ignore next */
3453
};
3554

36-
export const importAndScaleImage = async (cfg: {
55+
export const scaleImageAndWrite = async ({
56+
scaleFactor,
57+
path,
58+
}: {
3759
scaleFactor: number;
3860
path: string;
3961
}) => {
40-
const imgBuffer = fs.readFileSync(cfg.path);
41-
const rawImgNew = PNG.sync.read(imgBuffer);
42-
if (cfg.scaleFactor === 1) return rawImgNew;
62+
const imgBuffer = fs.readFileSync(path);
63+
if (scaleFactor === 1) return imgBuffer;
4364

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);
65+
const rawImgNew = PNG.sync.read(imgBuffer);
66+
const newImageWidth = Math.ceil(rawImgNew.width * scaleFactor);
67+
const newImageHeight = Math.ceil(rawImgNew.height * scaleFactor);
68+
await sharp(imgBuffer).resize(newImageWidth, newImageHeight).toFile(path);
4769

48-
return PNG.sync.read(fs.readFileSync(cfg.path));
70+
return fs.readFileSync(path);
4971
};
5072

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

src/plugins.ts

Lines changed: 1 addition & 1 deletion
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
};

0 commit comments

Comments
 (0)