UNPKG

@cappa/core

Version:

Core Playwright screenshot functionality for Cappa

376 lines (296 loc) 12.3 kB
import fs from "node:fs"; import path from "node:path"; import { PNG } from "pngjs"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { type CompareOptions, type CompareResult, compareImages, imagesMatch, saveDiffImage, } from "./compare"; // Test utilities function createSolidColorPNG( width: number, height: number, color: [number, number, number, number] = [255, 0, 0, 255], ): Buffer { const png = new PNG({ width, height }); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = (width * y + x) << 2; png.data[idx] = color[0]; // red png.data[idx + 1] = color[1]; // green png.data[idx + 2] = color[2]; // blue png.data[idx + 3] = color[3]; // alpha } } return PNG.sync.write(png); } function createGradientPNG(width: number, height: number): Buffer { const png = new PNG({ width, height }); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = (width * y + x) << 2; png.data[idx] = Math.floor((x / width) * 255); // red gradient png.data[idx + 1] = Math.floor((y / height) * 255); // green gradient png.data[idx + 2] = 128; // blue constant png.data[idx + 3] = 255; // alpha } } return PNG.sync.write(png); } function createCheckerboardPNG( width: number, height: number, squareSize: number = 10, ): Buffer { const png = new PNG({ width, height }); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = (width * y + x) << 2; const squareX = Math.floor(x / squareSize); const squareY = Math.floor(y / squareSize); const isWhite = (squareX + squareY) % 2 === 0; const color = isWhite ? 255 : 0; png.data[idx] = color; // red png.data[idx + 1] = color; // green png.data[idx + 2] = color; // blue png.data[idx + 3] = 255; // alpha } } return PNG.sync.write(png); } describe("compare", () => { const testDir = path.join(__dirname, "__test-images__"); beforeAll(() => { // Create test directory if (!fs.existsSync(testDir)) { fs.mkdirSync(testDir, { recursive: true }); } // Create test image files const redImage = createSolidColorPNG(100, 100, [255, 0, 0, 255]); const blueImage = createSolidColorPNG(100, 100, [0, 0, 255, 255]); const gradientImage = createGradientPNG(100, 100); const checkerImage = createCheckerboardPNG(100, 100); const smallImage = createSolidColorPNG(50, 50, [255, 0, 0, 255]); fs.writeFileSync(path.join(testDir, "red.png"), redImage); fs.writeFileSync(path.join(testDir, "blue.png"), blueImage); fs.writeFileSync(path.join(testDir, "gradient.png"), gradientImage); fs.writeFileSync(path.join(testDir, "checker.png"), checkerImage); fs.writeFileSync(path.join(testDir, "small.png"), smallImage); }); afterAll(() => { // Clean up test directory if (fs.existsSync(testDir)) { fs.rmSync(testDir, { recursive: true, force: true }); } }); describe("compareImages", () => { it("should return 0% difference for identical images", async () => { const image = createSolidColorPNG(50, 50, [255, 0, 0, 255]); const result = await compareImages(image, image); expect(result.numDiffPixels).toBe(0); expect(result.totalPixels).toBe(2500); expect(result.percentDifference).toBe(0); expect(result.passed).toBe(true); expect(result.diffBuffer).toEqual(undefined); }); it("should return 100% difference for completely different images", async () => { const redImage = createSolidColorPNG(50, 50, [255, 0, 0, 255]); const blueImage = createSolidColorPNG(50, 50, [0, 0, 255, 255]); const result = await compareImages(redImage, blueImage); expect(result.numDiffPixels).toBe(2500); expect(result.totalPixels).toBe(2500); expect(result.percentDifference).toBe(100); expect(result.passed).toBe(false); }); it("should work with file paths", async () => { const redPath = path.join(testDir, "red.png"); const bluePath = path.join(testDir, "blue.png"); const result = await compareImages(redPath, bluePath); expect(result.numDiffPixels).toBe(10000); expect(result.totalPixels).toBe(10000); expect(result.percentDifference).toBe(100); expect(result.passed).toBe(false); }); it("should work with mixed file path and buffer", async () => { const redPath = path.join(testDir, "red.png"); const blueBuffer = createSolidColorPNG(100, 100, [0, 0, 255, 255]); const result = await compareImages(redPath, blueBuffer); expect(result.percentDifference).toBe(100); expect(result.passed).toBe(false); }); it("should respect maxDiffPercentage parameter", async () => { const redImage = createSolidColorPNG(50, 50, [255, 0, 0, 255]); const blueImage = createSolidColorPNG(50, 50, [0, 0, 255, 255]); const result1 = await compareImages(redImage, blueImage, false, { maxDiffPercentage: 50, }); const result2 = await compareImages(redImage, blueImage, false, { maxDiffPercentage: 100, }); expect(result1.passed).toBe(false); // 100% > 50% expect(result2.passed).toBe(true); // 100% < 100% }); it("should apply threshold option correctly", async () => { // Create two slightly different images const image1 = createSolidColorPNG(50, 50, [100, 100, 100, 255]); const image2 = createSolidColorPNG(50, 50, [105, 105, 105, 255]); // Slightly lighter const strictResult = await compareImages(image1, image2, false, { threshold: 0.01, }); const lenientResult = await compareImages(image1, image2, false, { threshold: 0.5, }); expect(strictResult.numDiffPixels).toBeGreaterThan( lenientResult.numDiffPixels, ); }); it("should throw error for mismatched dimensions", async () => { const largeImage = createSolidColorPNG(100, 100, [255, 0, 0, 255]); const smallImage = createSolidColorPNG(50, 50, [255, 0, 0, 255]); const result = await compareImages(largeImage, smallImage); expect(result.differentSizes).toBe(true); expect(result.numDiffPixels).toBe(0); expect(result.totalPixels).toBe(0); expect(result.percentDifference).toBe(0); expect(result.passed).toBe(false); }); it("should handle custom compare options", async () => { const image1 = createCheckerboardPNG(50, 50, 5); const image2 = createCheckerboardPNG(50, 50, 6); // Slightly different pattern const options: CompareOptions = { threshold: 0.2, includeAA: true, alpha: 0.2, aaColor: [0, 255, 0], diffColor: [255, 0, 255], diffColorAlt: [0, 255, 255], }; const result = await compareImages(image1, image2, false, options); expect(result).toBeDefined(); expect(result.diffBuffer).toEqual(undefined); }); it("should handle identical file paths", async () => { const redPath = path.join(testDir, "red.png"); const result = await compareImages(redPath, redPath); expect(result.numDiffPixels).toBe(0); expect(result.percentDifference).toBe(0); expect(result.passed).toBe(true); }); }); describe("imagesMatch", () => { it("should return true for identical images", async () => { const image = createSolidColorPNG(50, 50, [255, 0, 0, 255]); const matches = await imagesMatch(image, image); expect(matches).toBe(true); }); it("should return false for completely different images", async () => { const redImage = createSolidColorPNG(50, 50, [255, 0, 0, 255]); const blueImage = createSolidColorPNG(50, 50, [0, 0, 255, 255]); const matches = await imagesMatch(redImage, blueImage); expect(matches).toBe(false); }); it("should respect maxDifferencePercent", async () => { const redImage = createSolidColorPNG(50, 50, [255, 0, 0, 255]); const blueImage = createSolidColorPNG(50, 50, [0, 0, 255, 255]); const strictMatch = await imagesMatch(redImage, blueImage, { maxDiffPercentage: 50, }); const lenientMatch = await imagesMatch(redImage, blueImage, { maxDiffPercentage: 150, }); expect(strictMatch).toBe(false); expect(lenientMatch).toBe(true); }); it("should work with file paths", async () => { const redPath = path.join(testDir, "red.png"); const checkerPath = path.join(testDir, "checker.png"); const matches = await imagesMatch(redPath, checkerPath); expect(matches).toBe(false); }); it("should pass through compare options", async () => { const image1 = createSolidColorPNG(50, 50, [100, 100, 100, 255]); const image2 = createSolidColorPNG(50, 50, [105, 105, 105, 255]); const strictMatch = await imagesMatch(image1, image2, { maxDiffPercentage: 50, threshold: 0.01, }); const lenientMatch = await imagesMatch(image1, image2, { maxDiffPercentage: 50, threshold: 0.5, }); // With lenient threshold, more pixels should be considered "same" expect(lenientMatch).toBe(true); expect(strictMatch).toBe(false); }); }); describe("saveDiffImage", () => { it("should save diff image to file", async () => { const redImage = createSolidColorPNG(50, 50, [255, 0, 0, 255]); const blueImage = createSolidColorPNG(50, 50, [0, 0, 255, 255]); const result = await compareImages(redImage, blueImage, true); const outputPath = path.join(testDir, "diff-output.png"); saveDiffImage(result, outputPath); expect(fs.existsSync(outputPath)).toBe(true); // Verify the saved file is a valid PNG const savedBuffer = fs.readFileSync(outputPath); expect(() => PNG.sync.read(savedBuffer)).not.toThrow(); // Clean up fs.unlinkSync(outputPath); }); it("should throw error when no diff buffer available", () => { const result: CompareResult = { numDiffPixels: 0, totalPixels: 100, percentDifference: 0, passed: true, differentSizes: false, // diffBuffer is undefined }; expect(() => saveDiffImage(result, "test.png")).toThrow( "No diff buffer available in comparison result", ); }); }); describe("error handling", () => { it("should handle non-existent file paths", async () => { const nonExistentPath = path.join(testDir, "does-not-exist.png"); const validImage = createSolidColorPNG(50, 50, [255, 0, 0, 255]); await expect( compareImages(nonExistentPath, validImage), ).rejects.toThrow(); }); it("should handle invalid image buffers", async () => { const invalidBuffer = Buffer.from("not a png"); const validImage = createSolidColorPNG(50, 50, [255, 0, 0, 255]); await expect(compareImages(invalidBuffer, validImage)).rejects.toThrow(); }); }); describe("edge cases", () => { it("should handle 1x1 pixel images", async () => { const image1 = createSolidColorPNG(1, 1, [255, 0, 0, 255]); const image2 = createSolidColorPNG(1, 1, [0, 0, 255, 255]); const result = await compareImages(image1, image2); expect(result.totalPixels).toBe(1); expect(result.numDiffPixels).toBe(1); expect(result.percentDifference).toBe(100); }); it("should handle very large difference percentages", async () => { const redImage = createSolidColorPNG(10, 10, [255, 0, 0, 255]); const blueImage = createSolidColorPNG(10, 10, [0, 0, 255, 255]); const result = await compareImages(redImage, blueImage, false); expect(result.passed).toBe(false); expect(result.percentDifference).toBe(100); }); it("should handle zero difference threshold", async () => { const image = createSolidColorPNG(10, 10, [255, 0, 0, 255]); const result = await compareImages(image, image, false, { maxDiffPercentage: 0, }); expect(result.passed).toBe(true); expect(result.percentDifference).toBe(0); }); }); });