From c3b44a4c5950164f4c3629f9eb1587b862a9c206 Mon Sep 17 00:00:00 2001 From: Jakub Freisler Date: Sat, 12 Nov 2022 19:56:34 +0100 Subject: [PATCH 1/2] fix: autocleanup removes screenshots of other testing type add test case to cover this scenario add isImageOfTestType check resolves #178 Signed-off-by: Jakub Freisler --- __tests__/fixtures/screenshot.actual.png | Bin 62146 -> 62214 bytes __tests__/fixtures/screenshot.png | Bin 44638 -> 44706 bytes src/image.utils.ts | 44 +++++++++---- src/task.hook.test.ts | 79 +++++++++++++++-------- src/task.hook.ts | 10 +-- 5 files changed, 92 insertions(+), 41 deletions(-) diff --git a/__tests__/fixtures/screenshot.actual.png b/__tests__/fixtures/screenshot.actual.png index 9342c2a7ca41ec9ac7e827fb9a7d696e24ce2ab8..5d797056f1c74b8b71639da9ee531683725f0eb5 100644 GIT binary patch delta 78 zcmX@~l)3F0^8^(~ixSs}61SjW|Ii?3*LdfEuppQCFotTSvecsD%=|nhDkOVGPwuWvNBQnfZB2R!YWthI*z- fI!Yy}#U+_}=^>Q`sX) - addMetadata(png, METADATA_KEY, version /* c8 ignore next */); -export const getPNGMetadata = (png: Buffer) => - getMetadata(png, METADATA_KEY /* c8 ignore next */); +type PluginMetadata = { + version: string; + testingType?: 'e2e' | 'component'; +}; + +type PluginMetadataConfig = { + testingType?: string; +}; + +export const addPNGMetadata = (config: PluginMetadataConfig, png: Buffer) => + addMetadata(png, METADATA_KEY, JSON.stringify({ version, testingType: config.testingType || 'e2e' } as PluginMetadata) /* c8 ignore next */); +export const getPNGMetadata = (png: Buffer): PluginMetadata | undefined => { + const metadataString = getMetadata(png, METADATA_KEY /* c8 ignore next */); + + if (metadataString === undefined) return; + try { + return JSON.parse(metadataString); + } catch { + return { version: metadataString }; + } +} export const isImageCurrentVersion = (png: Buffer) => - getPNGMetadata(png) === version; + getPNGMetadata(png)?.version === version; export const isImageGeneratedByPlugin = (png: Buffer) => !!getPNGMetadata(png /* c8 ignore next */); +export const isImageOfTestType = (png: Buffer, testingType?: PluginMetadataConfig['testingType']) => { + if (!isImageGeneratedByPlugin(png)) return false; + const imageTestingType = getPNGMetadata(png /* c8 ignore next */)?.testingType; + return imageTestingType === testingType || testingType === imageTestingType === undefined; +}; -export const writePNG = (name: string, png: PNG | Buffer) => +export const writePNG = (config: PluginMetadataConfig, name: string, png: PNG | Buffer) => fs.writeFileSync( name, - addPNGMetadata(png instanceof PNG ? PNG.sync.write(png) : png) + addPNGMetadata(config, png instanceof PNG ? PNG.sync.write(png) : png) ); const inArea = (x: number, y: number, height: number, width: number) => @@ -95,17 +117,17 @@ export const alignImagesToSameSize = ( ]; }; -export const cleanupUnused = (rootPath: string) => { +export const cleanupUnused = (config: PluginMetadataConfig & { projectRoot: string; }) => { glob .sync("**/*.png", { - cwd: rootPath, + cwd: config.projectRoot, ignore: "node_modules/**/*", }) .forEach((pngPath) => { - const absolutePath = path.join(rootPath, pngPath); + const absolutePath = path.join(config.projectRoot, pngPath); if ( !wasScreenshotUsed(pngPath) && - isImageGeneratedByPlugin(fs.readFileSync(absolutePath)) + isImageOfTestType(fs.readFileSync(absolutePath), config.testingType) ) { fs.unlinkSync(absolutePath); } diff --git a/src/task.hook.test.ts b/src/task.hook.test.ts index 3a436b03..58cfe5de 100644 --- a/src/task.hook.test.ts +++ b/src/task.hook.test.ts @@ -48,6 +48,7 @@ describe("getScreenshotPathInfoTask", () => { titleFromOptions: "some-title-withśpęćiał人物", imagesPath: "nested/images/dir", specPath, + currentRetryNumber: 0, }) ).toEqual({ screenshotPath: @@ -62,6 +63,7 @@ describe("getScreenshotPathInfoTask", () => { titleFromOptions: "some-title", imagesPath: "{spec_path}/images/dir", specPath, + currentRetryNumber: 0, }) ).toEqual({ screenshotPath: @@ -76,6 +78,7 @@ describe("getScreenshotPathInfoTask", () => { titleFromOptions: "some-title", imagesPath: "/images/dir", specPath, + currentRetryNumber: 0, }) ).toEqual({ screenshotPath: @@ -88,6 +91,7 @@ describe("getScreenshotPathInfoTask", () => { titleFromOptions: "some-title", imagesPath: "C:/images/dir", specPath, + currentRetryNumber: 0, }) ).toEqual({ screenshotPath: @@ -104,6 +108,7 @@ describe("cleanupImagesTask", () => { titleFromOptions: "some-file", imagesPath: "images", specPath: "some/spec/path", + currentRetryNumber: 0, }); return path.join( projectRoot, @@ -113,34 +118,56 @@ describe("cleanupImagesTask", () => { ); }; - it("does not remove used screenshot", async () => { - const { path: projectRoot } = await dir(); - const screenshotPath = await writeTmpFixture( - await generateUsedScreenshotPath(projectRoot), - oldImgFixture - ); + describe('when testing type does not match', () => { + it("does not remove unused screenshot", async () => { + const { path: projectRoot } = await dir(); + const screenshotPath = await writeTmpFixture( + path.join(projectRoot, "some-file-2 #0.png"), + oldImgFixture + ); - cleanupImagesTask({ - projectRoot, - env: { pluginVisualRegressionCleanupUnusedImages: true }, - } as unknown as Cypress.PluginConfigOptions); + cleanupImagesTask({ + projectRoot, + env: { pluginVisualRegressionCleanupUnusedImages: true }, + testingType: 'component', + } as unknown as Cypress.PluginConfigOptions); - expect(existsSync(screenshotPath)).toBe(true); + expect(existsSync(screenshotPath)).toBe(true); + }); }); - it("removes unused screenshot", async () => { - const { path: projectRoot } = await dir(); - const screenshotPath = await writeTmpFixture( - path.join(projectRoot, "some-file-2 #0.png"), - oldImgFixture - ); + describe('when testing type matches', () => { + it("does not remove used screenshot", async () => { + const { path: projectRoot } = await dir(); + const screenshotPath = await writeTmpFixture( + await generateUsedScreenshotPath(projectRoot), + oldImgFixture + ); - cleanupImagesTask({ - projectRoot, - env: { pluginVisualRegressionCleanupUnusedImages: true }, - } as unknown as Cypress.PluginConfigOptions); + cleanupImagesTask({ + projectRoot, + env: { pluginVisualRegressionCleanupUnusedImages: true }, + testingType: 'e2e', + } as unknown as Cypress.PluginConfigOptions); - expect(existsSync(screenshotPath)).toBe(false); + expect(existsSync(screenshotPath)).toBe(true); + }); + + it("removes unused screenshot", async () => { + const { path: projectRoot } = await dir(); + const screenshotPath = await writeTmpFixture( + path.join(projectRoot, "some-file-2 #0.png"), + oldImgFixture + ); + + cleanupImagesTask({ + projectRoot, + env: { pluginVisualRegressionCleanupUnusedImages: true }, + testingType: 'e2e', + } as unknown as Cypress.PluginConfigOptions); + + expect(existsSync(screenshotPath)).toBe(false); + }); }); }); }); @@ -178,7 +205,7 @@ describe("compareImagesTask", () => { describe("when old screenshot exists", () => { it("resolves with a success message", async () => expect( - compareImagesTask(await generateConfig({ updateImages: true })) + compareImagesTask({ testingType: 'e2e' }, await generateConfig({ updateImages: true })) ).resolves.toEqual({ message: "Image diff factor (0%) is within boundaries of maximum threshold option 0.5.", @@ -197,7 +224,7 @@ describe("compareImagesTask", () => { const cfg = await generateConfig({ updateImages: false }); await fs.unlink(cfg.imgOld); - await expect(compareImagesTask(cfg)).resolves.toEqual({ + await expect(compareImagesTask({ testingType: 'e2e' }, cfg)).resolves.toEqual({ message: "Image diff factor (0%) is within boundaries of maximum threshold option 0.5.", imgDiff: 0, @@ -214,7 +241,7 @@ describe("compareImagesTask", () => { it("resolves with an error message", async () => { const cfg = await generateConfig({ updateImages: false }); - await expect(compareImagesTask(cfg)).resolves.toMatchSnapshot(); + await expect(compareImagesTask({ testingType: 'e2e' }, cfg)).resolves.toMatchSnapshot(); }); }); @@ -223,7 +250,7 @@ describe("compareImagesTask", () => { const cfg = await generateConfig({ updateImages: false }); await writeTmpFixture(cfg.imgNew, oldImgFixture); - await expect(compareImagesTask(cfg)).resolves.toMatchSnapshot(); + await expect(compareImagesTask({ testingType: 'e2e' }, cfg)).resolves.toMatchSnapshot(); }); }); }); diff --git a/src/task.hook.ts b/src/task.hook.ts index 64f4ccba..74091d43 100644 --- a/src/task.hook.ts +++ b/src/task.hook.ts @@ -47,7 +47,7 @@ export const getScreenshotPathInfoTask = (cfg: { export const cleanupImagesTask = (config: Cypress.PluginConfigOptions) => { if (config.env["pluginVisualRegressionCleanupUnusedImages"]) { - cleanupUnused(config.projectRoot); + cleanupUnused(config); } resetScreenshotNameCache(); @@ -68,6 +68,7 @@ export const approveImageTask = ({ img }: { img: string }) => { }; export const compareImagesTask = async ( + cypressConfig: { testingType: string }, cfg: CompareImagesCfg ): Promise => { const messages = [] as string[]; @@ -127,6 +128,7 @@ export const compareImagesTask = async ( if (error) { writePNG( + cypressConfig, cfg.imgNew.replace(FILE_SUFFIX.actual, FILE_SUFFIX.diff), diffBuffer ); @@ -141,7 +143,7 @@ export const compareImagesTask = async ( }; } else { if (rawImgOld && !isImageCurrentVersion(rawImgOldBuffer)) { - writePNG(cfg.imgNew, rawImgNewBuffer); + writePNG(cypressConfig, cfg.imgNew, rawImgNewBuffer); moveFile.sync(cfg.imgNew, cfg.imgOld); } else { // don't overwrite file if it's the same (imgDiff < cfg.maxDiffThreshold && !isImgSizeDifferent) @@ -154,7 +156,7 @@ export const compareImagesTask = async ( imgNewBase64 = ""; imgDiffBase64 = ""; imgOldBase64 = ""; - writePNG(cfg.imgNew, rawImgNewBuffer); + writePNG(cypressConfig, cfg.imgNew, rawImgNewBuffer); moveFile.sync(cfg.imgNew, cfg.imgOld); } @@ -189,6 +191,6 @@ export const initTaskHook = (config: Cypress.PluginConfigOptions) => ({ [TASK.cleanupImages]: cleanupImagesTask.bind(undefined, config), [TASK.doesFileExist]: doesFileExistTask, [TASK.approveImage]: approveImageTask, - [TASK.compareImages]: compareImagesTask, + [TASK.compareImages]: compareImagesTask.bind(undefined, config), }); /* c8 ignore stop */ From c7823efdb651701f1e149d93a8ab9c6d5f3c8450 Mon Sep 17 00:00:00 2001 From: Jakub Freisler Date: Sat, 12 Nov 2022 20:00:26 +0100 Subject: [PATCH 2/2] chore: run linter Signed-off-by: Jakub Freisler --- src/image.utils.ts | 37 +++++++++++++++++++++++++++++-------- src/task.hook.test.ts | 27 ++++++++++++++++++--------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/image.utils.ts b/src/image.utils.ts index 4f5af2e2..b87a5be0 100644 --- a/src/image.utils.ts +++ b/src/image.utils.ts @@ -10,7 +10,7 @@ import { METADATA_KEY } from "./constants"; type PluginMetadata = { version: string; - testingType?: 'e2e' | 'component'; + testingType?: "e2e" | "component"; }; type PluginMetadataConfig = { @@ -18,7 +18,14 @@ type PluginMetadataConfig = { }; export const addPNGMetadata = (config: PluginMetadataConfig, png: Buffer) => - addMetadata(png, METADATA_KEY, JSON.stringify({ version, testingType: config.testingType || 'e2e' } as PluginMetadata) /* c8 ignore next */); + addMetadata( + png, + METADATA_KEY, + JSON.stringify({ + version, + testingType: config.testingType || "e2e", + } as PluginMetadata) /* c8 ignore next */ + ); export const getPNGMetadata = (png: Buffer): PluginMetadata | undefined => { const metadataString = getMetadata(png, METADATA_KEY /* c8 ignore next */); @@ -28,18 +35,30 @@ export const getPNGMetadata = (png: Buffer): PluginMetadata | undefined => { } catch { return { version: metadataString }; } -} +}; export const isImageCurrentVersion = (png: Buffer) => getPNGMetadata(png)?.version === version; export const isImageGeneratedByPlugin = (png: Buffer) => !!getPNGMetadata(png /* c8 ignore next */); -export const isImageOfTestType = (png: Buffer, testingType?: PluginMetadataConfig['testingType']) => { +export const isImageOfTestType = ( + png: Buffer, + testingType?: PluginMetadataConfig["testingType"] +) => { if (!isImageGeneratedByPlugin(png)) return false; - const imageTestingType = getPNGMetadata(png /* c8 ignore next */)?.testingType; - return imageTestingType === testingType || testingType === imageTestingType === undefined; + const imageTestingType = getPNGMetadata( + png /* c8 ignore next */ + )?.testingType; + return ( + imageTestingType === testingType || + (testingType === imageTestingType) === undefined + ); }; -export const writePNG = (config: PluginMetadataConfig, name: string, png: PNG | Buffer) => +export const writePNG = ( + config: PluginMetadataConfig, + name: string, + png: PNG | Buffer +) => fs.writeFileSync( name, addPNGMetadata(config, png instanceof PNG ? PNG.sync.write(png) : png) @@ -117,7 +136,9 @@ export const alignImagesToSameSize = ( ]; }; -export const cleanupUnused = (config: PluginMetadataConfig & { projectRoot: string; }) => { +export const cleanupUnused = ( + config: PluginMetadataConfig & { projectRoot: string } +) => { glob .sync("**/*.png", { cwd: config.projectRoot, diff --git a/src/task.hook.test.ts b/src/task.hook.test.ts index 58cfe5de..ed11d78b 100644 --- a/src/task.hook.test.ts +++ b/src/task.hook.test.ts @@ -118,7 +118,7 @@ describe("cleanupImagesTask", () => { ); }; - describe('when testing type does not match', () => { + describe("when testing type does not match", () => { it("does not remove unused screenshot", async () => { const { path: projectRoot } = await dir(); const screenshotPath = await writeTmpFixture( @@ -129,14 +129,14 @@ describe("cleanupImagesTask", () => { cleanupImagesTask({ projectRoot, env: { pluginVisualRegressionCleanupUnusedImages: true }, - testingType: 'component', + testingType: "component", } as unknown as Cypress.PluginConfigOptions); expect(existsSync(screenshotPath)).toBe(true); }); }); - describe('when testing type matches', () => { + describe("when testing type matches", () => { it("does not remove used screenshot", async () => { const { path: projectRoot } = await dir(); const screenshotPath = await writeTmpFixture( @@ -147,7 +147,7 @@ describe("cleanupImagesTask", () => { cleanupImagesTask({ projectRoot, env: { pluginVisualRegressionCleanupUnusedImages: true }, - testingType: 'e2e', + testingType: "e2e", } as unknown as Cypress.PluginConfigOptions); expect(existsSync(screenshotPath)).toBe(true); @@ -163,7 +163,7 @@ describe("cleanupImagesTask", () => { cleanupImagesTask({ projectRoot, env: { pluginVisualRegressionCleanupUnusedImages: true }, - testingType: 'e2e', + testingType: "e2e", } as unknown as Cypress.PluginConfigOptions); expect(existsSync(screenshotPath)).toBe(false); @@ -205,7 +205,10 @@ describe("compareImagesTask", () => { describe("when old screenshot exists", () => { it("resolves with a success message", async () => expect( - compareImagesTask({ testingType: 'e2e' }, await generateConfig({ updateImages: true })) + compareImagesTask( + { testingType: "e2e" }, + await generateConfig({ updateImages: true }) + ) ).resolves.toEqual({ message: "Image diff factor (0%) is within boundaries of maximum threshold option 0.5.", @@ -224,7 +227,9 @@ describe("compareImagesTask", () => { const cfg = await generateConfig({ updateImages: false }); await fs.unlink(cfg.imgOld); - await expect(compareImagesTask({ testingType: 'e2e' }, cfg)).resolves.toEqual({ + await expect( + compareImagesTask({ testingType: "e2e" }, cfg) + ).resolves.toEqual({ message: "Image diff factor (0%) is within boundaries of maximum threshold option 0.5.", imgDiff: 0, @@ -241,7 +246,9 @@ describe("compareImagesTask", () => { it("resolves with an error message", async () => { const cfg = await generateConfig({ updateImages: false }); - await expect(compareImagesTask({ testingType: 'e2e' }, cfg)).resolves.toMatchSnapshot(); + await expect( + compareImagesTask({ testingType: "e2e" }, cfg) + ).resolves.toMatchSnapshot(); }); }); @@ -250,7 +257,9 @@ describe("compareImagesTask", () => { const cfg = await generateConfig({ updateImages: false }); await writeTmpFixture(cfg.imgNew, oldImgFixture); - await expect(compareImagesTask({ testingType: 'e2e' }, cfg)).resolves.toMatchSnapshot(); + await expect( + compareImagesTask({ testingType: "e2e" }, cfg) + ).resolves.toMatchSnapshot(); }); }); });