diff --git a/__tests__/fixtures/screenshot.actual.png b/__tests__/fixtures/screenshot.actual.png index 9342c2a7..5d797056 100644 Binary files a/__tests__/fixtures/screenshot.actual.png and b/__tests__/fixtures/screenshot.actual.png differ diff --git a/__tests__/fixtures/screenshot.png b/__tests__/fixtures/screenshot.png index ccb685a5..dd94dfe8 100644 Binary files a/__tests__/fixtures/screenshot.png and b/__tests__/fixtures/screenshot.png differ diff --git a/src/image.utils.ts b/src/image.utils.ts index 25a2fc2d..b87a5be0 100644 --- a/src/image.utils.ts +++ b/src/image.utils.ts @@ -8,19 +8,60 @@ import { version } from "../package.json"; import { wasScreenshotUsed } from "./screenshotPath.utils"; import { METADATA_KEY } from "./constants"; -export const addPNGMetadata = (png: Buffer) => - 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 +136,19 @@ 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..ed11d78b 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,10 @@ 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 +227,9 @@ 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 +246,9 @@ 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 +257,9 @@ 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 */