From e0a4d7fa038cbf688881dd46c0c5eb6d437ae932 Mon Sep 17 00:00:00 2001 From: Noah Lindner Date: Sat, 25 Jul 2026 21:40:55 -0400 Subject: [PATCH 1/4] fix(looks-same): expose native diff percentage --- .../looks-same/looks-same.service.spec.ts | 42 +++++++++++++------ .../libs/looks-same/looks-same.service.ts | 30 +++++-------- .../libs/looks-same/looks-same.types.ts | 14 +++++++ 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/compare/libs/looks-same/looks-same.service.spec.ts b/src/compare/libs/looks-same/looks-same.service.spec.ts index 505ceb3e..2b069fe3 100644 --- a/src/compare/libs/looks-same/looks-same.service.spec.ts +++ b/src/compare/libs/looks-same/looks-same.service.spec.ts @@ -122,7 +122,12 @@ describe('getDiff', () => { it('diff not found', async () => { const getImageMock = jest.fn().mockReturnValueOnce(image).mockReturnValueOnce(anotherImage); service = await initService({ getImageMock }); - service.compare = jest.fn().mockReturnValueOnce({ equal: true }); + service.compare = jest.fn().mockReturnValueOnce({ + equal: true, + differentPixels: 0, + totalPixels: 400, + diffImage: null, + }); const result = await service.getDiff( { @@ -138,8 +143,8 @@ describe('getDiff', () => { expect(result).toStrictEqual({ status: TestStatus.ok, diffName: null, - diffPercent: undefined, - pixelMisMatchCount: undefined, + diffPercent: 0, + pixelMisMatchCount: 0, isSameDimension: true, }); }); @@ -147,7 +152,12 @@ describe('getDiff', () => { it('diff found', async () => { const getImageMock = jest.fn().mockReturnValueOnce(image).mockReturnValueOnce(anotherImage); service = await initService({ getImageMock }); - service.compare = jest.fn().mockReturnValueOnce({ equal: false }); + service.compare = jest.fn().mockReturnValueOnce({ + equal: false, + differentPixels: 20, + totalPixels: 400, + diffImage: null, + }); const result = await service.getDiff( { @@ -164,17 +174,24 @@ describe('getDiff', () => { expect(result).toStrictEqual({ status: TestStatus.unresolved, diffName: null, - diffPercent: undefined, - pixelMisMatchCount: undefined, + diffPercent: 5, + pixelMisMatchCount: 20, isSameDimension: true, }); }); it('diff found and save diff', async () => { const getImageMock = jest.fn().mockReturnValueOnce(image).mockReturnValueOnce(anotherImage); - service = await initService({ getImageMock }); - service.compare = jest.fn().mockReturnValueOnce({ equal: false }); - service.createDiff = jest.fn().mockReturnValueOnce('diff name'); + const saveImageMock = jest.fn().mockReturnValueOnce('diff name'); + const diffBuffer = Buffer.from('diff'); + const createBuffer = jest.fn().mockReturnValueOnce(diffBuffer); + service = await initService({ getImageMock, saveImageMock }); + service.compare = jest.fn().mockReturnValueOnce({ + equal: false, + differentPixels: 20, + totalPixels: 400, + diffImage: { createBuffer }, + }); const result = await service.getDiff( { @@ -188,12 +205,13 @@ describe('getDiff', () => { ); expect(service.compare).toHaveBeenCalledWith(image, anotherImage, DEFAULT_CONFIG); - expect(service.createDiff).toHaveBeenCalledWith(image, anotherImage, DEFAULT_CONFIG); + expect(createBuffer).toHaveBeenCalledWith('png'); + expect(saveImageMock).toHaveBeenCalledWith('diff', diffBuffer); expect(result).toStrictEqual({ status: TestStatus.unresolved, diffName: 'diff name', - diffPercent: undefined, - pixelMisMatchCount: undefined, + diffPercent: 5, + pixelMisMatchCount: 20, isSameDimension: true, }); }); diff --git a/src/compare/libs/looks-same/looks-same.service.ts b/src/compare/libs/looks-same/looks-same.service.ts index 2127e02d..a43e3edc 100644 --- a/src/compare/libs/looks-same/looks-same.service.ts +++ b/src/compare/libs/looks-same/looks-same.service.ts @@ -54,12 +54,16 @@ export class LookSameService implements ImageComparator { // compare const compareResult = await this.compare(baselineIgnored, imageIgnored, config); + result.pixelMisMatchCount = compareResult.differentPixels; + result.diffPercent = + compareResult.totalPixels > 0 ? (compareResult.differentPixels * 100) / compareResult.totalPixels : 0; + if (compareResult.equal) { result.status = TestStatus.ok; } else { result.status = TestStatus.unresolved; - if (data.saveDiffAsFile) { - result.diffName = await this.createDiff(baselineIgnored, imageIgnored, config); + if (data.saveDiffAsFile && compareResult.diffImage) { + result.diffName = await this.staticService.saveImage('diff', await compareResult.diffImage.createBuffer('png')); } } @@ -67,7 +71,10 @@ export class LookSameService implements ImageComparator { } async compare(baseline: PNG, image: PNG, config: LooksSameConfig): Promise { - const diffResult = await looksSame(PNG.sync.write(baseline), PNG.sync.write(image), config).catch((error) => { + const diffResult = await looksSame(PNG.sync.write(baseline), PNG.sync.write(image), { + ...config, + createDiffImage: true, + }).catch((error) => { this.logger.error(error.message); }); if (diffResult) { @@ -75,21 +82,4 @@ export class LookSameService implements ImageComparator { } return undefined; } - - async createDiff(baseline: PNG, image: PNG, config: LooksSameConfig): Promise { - const buffer = await looksSame - .createDiff({ - reference: PNG.sync.write(baseline), - current: PNG.sync.write(image), - highlightColor: '#ff00ff', - ...config, - }) - .catch((error) => { - this.logger.error(error.message); - }); - if (buffer) { - return this.staticService.saveImage('diff', buffer); - } - return undefined; - } } diff --git a/src/compare/libs/looks-same/looks-same.types.ts b/src/compare/libs/looks-same/looks-same.types.ts index e25751fc..936bde25 100644 --- a/src/compare/libs/looks-same/looks-same.types.ts +++ b/src/compare/libs/looks-same/looks-same.types.ts @@ -57,4 +57,18 @@ export type LookSameResult = { * diff clusters for not equal images */ diffClusters?: CoordBounds[]; + /** + * number of pixels considered different + */ + differentPixels: number; + /** + * number of pixels compared + */ + totalPixels: number; + /** + * generated diff image when createDiffImage is enabled + */ + diffImage: { + createBuffer(extension: 'png' | 'raw'): Promise; + } | null; }; From db7d65d48c3097fb3395d35799e1695213bfd599 Mon Sep 17 00:00:00 2001 From: Noah Lindner Date: Sat, 25 Jul 2026 21:43:41 -0400 Subject: [PATCH 2/4] fix(looks-same): preserve diff artifact invariant --- .../looks-same/looks-same.service.spec.ts | 2 +- .../libs/looks-same/looks-same.service.ts | 2 +- .../libs/looks-same/looks-same.types.ts | 19 +++++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/compare/libs/looks-same/looks-same.service.spec.ts b/src/compare/libs/looks-same/looks-same.service.spec.ts index 2b069fe3..639e3633 100644 --- a/src/compare/libs/looks-same/looks-same.service.spec.ts +++ b/src/compare/libs/looks-same/looks-same.service.spec.ts @@ -156,7 +156,7 @@ describe('getDiff', () => { equal: false, differentPixels: 20, totalPixels: 400, - diffImage: null, + diffImage: { createBuffer: jest.fn() }, }); const result = await service.getDiff( diff --git a/src/compare/libs/looks-same/looks-same.service.ts b/src/compare/libs/looks-same/looks-same.service.ts index a43e3edc..d6d73c0b 100644 --- a/src/compare/libs/looks-same/looks-same.service.ts +++ b/src/compare/libs/looks-same/looks-same.service.ts @@ -62,7 +62,7 @@ export class LookSameService implements ImageComparator { result.status = TestStatus.ok; } else { result.status = TestStatus.unresolved; - if (data.saveDiffAsFile && compareResult.diffImage) { + if (data.saveDiffAsFile) { result.diffName = await this.staticService.saveImage('diff', await compareResult.diffImage.createBuffer('png')); } } diff --git a/src/compare/libs/looks-same/looks-same.types.ts b/src/compare/libs/looks-same/looks-same.types.ts index 936bde25..47d77fd3 100644 --- a/src/compare/libs/looks-same/looks-same.types.ts +++ b/src/compare/libs/looks-same/looks-same.types.ts @@ -44,7 +44,7 @@ export interface CoordBounds { bottom: number; } -export type LookSameResult = { +type LookSameBaseResult = { /** * true if images are equal, false - otherwise */ @@ -68,7 +68,18 @@ export type LookSameResult = { /** * generated diff image when createDiffImage is enabled */ - diffImage: { - createBuffer(extension: 'png' | 'raw'): Promise; - } | null; }; + +type DiffImage = { + createBuffer(extension: 'png' | 'raw'): Promise; +}; + +export type LookSameResult = + | (LookSameBaseResult & { + equal: true; + diffImage: null; + }) + | (LookSameBaseResult & { + equal: false; + diffImage: DiffImage; + }); From 2f8154a981d39dd66106e90fb5a051e343cc09fa Mon Sep 17 00:00:00 2001 From: Noah Lindner Date: Sat, 25 Jul 2026 21:47:37 -0400 Subject: [PATCH 3/4] refactor(looks-same): keep metrics off comparison path --- .../looks-same/looks-same.service.spec.ts | 66 +++++++++++-------- .../libs/looks-same/looks-same.service.ts | 35 +++++++--- .../libs/looks-same/looks-same.types.ts | 27 +------- 3 files changed, 67 insertions(+), 61 deletions(-) diff --git a/src/compare/libs/looks-same/looks-same.service.spec.ts b/src/compare/libs/looks-same/looks-same.service.spec.ts index 639e3633..203019dc 100644 --- a/src/compare/libs/looks-same/looks-same.service.spec.ts +++ b/src/compare/libs/looks-same/looks-same.service.spec.ts @@ -122,12 +122,7 @@ describe('getDiff', () => { it('diff not found', async () => { const getImageMock = jest.fn().mockReturnValueOnce(image).mockReturnValueOnce(anotherImage); service = await initService({ getImageMock }); - service.compare = jest.fn().mockReturnValueOnce({ - equal: true, - differentPixels: 0, - totalPixels: 400, - diffImage: null, - }); + service.compare = jest.fn().mockReturnValueOnce({ equal: true }); const result = await service.getDiff( { @@ -143,8 +138,8 @@ describe('getDiff', () => { expect(result).toStrictEqual({ status: TestStatus.ok, diffName: null, - diffPercent: 0, - pixelMisMatchCount: 0, + diffPercent: undefined, + pixelMisMatchCount: undefined, isSameDimension: true, }); }); @@ -152,12 +147,7 @@ describe('getDiff', () => { it('diff found', async () => { const getImageMock = jest.fn().mockReturnValueOnce(image).mockReturnValueOnce(anotherImage); service = await initService({ getImageMock }); - service.compare = jest.fn().mockReturnValueOnce({ - equal: false, - differentPixels: 20, - totalPixels: 400, - diffImage: { createBuffer: jest.fn() }, - }); + service.compare = jest.fn().mockReturnValueOnce({ equal: false }); const result = await service.getDiff( { @@ -174,23 +164,20 @@ describe('getDiff', () => { expect(result).toStrictEqual({ status: TestStatus.unresolved, diffName: null, - diffPercent: 5, - pixelMisMatchCount: 20, + diffPercent: undefined, + pixelMisMatchCount: undefined, isSameDimension: true, }); }); it('diff found and save diff', async () => { const getImageMock = jest.fn().mockReturnValueOnce(image).mockReturnValueOnce(anotherImage); - const saveImageMock = jest.fn().mockReturnValueOnce('diff name'); - const diffBuffer = Buffer.from('diff'); - const createBuffer = jest.fn().mockReturnValueOnce(diffBuffer); - service = await initService({ getImageMock, saveImageMock }); - service.compare = jest.fn().mockReturnValueOnce({ - equal: false, - differentPixels: 20, - totalPixels: 400, - diffImage: { createBuffer }, + service = await initService({ getImageMock }); + service.compare = jest.fn().mockReturnValueOnce({ equal: false }); + service.createDiff = jest.fn().mockReturnValueOnce({ + diffName: 'diff name', + diffPercent: 5, + pixelMisMatchCount: 20, }); const result = await service.getDiff( @@ -205,8 +192,7 @@ describe('getDiff', () => { ); expect(service.compare).toHaveBeenCalledWith(image, anotherImage, DEFAULT_CONFIG); - expect(createBuffer).toHaveBeenCalledWith('png'); - expect(saveImageMock).toHaveBeenCalledWith('diff', diffBuffer); + expect(service.createDiff).toHaveBeenCalledWith(image, anotherImage, DEFAULT_CONFIG); expect(result).toStrictEqual({ status: TestStatus.unresolved, diffName: 'diff name', @@ -216,3 +202,29 @@ describe('getDiff', () => { }); }); }); + +describe('createDiff', () => { + it('returns metrics from the generated Looks-Same diff', async () => { + const baseline = new PNG({ width: 2, height: 1 }); + const image = new PNG({ width: 2, height: 1 }); + baseline.data.fill(255); + image.data.fill(255); + image.data[0] = 0; + + const saveImageMock = jest.fn().mockReturnValueOnce('diff name'); + service = await initService({ saveImageMock }); + + const result = await service.createDiff(baseline, image, { + ...DEFAULT_CONFIG, + strict: true, + ignoreAntialiasing: false, + }); + + expect(result).toStrictEqual({ + diffName: 'diff name', + diffPercent: 50, + pixelMisMatchCount: 1, + }); + expect(saveImageMock).toHaveBeenCalledWith('diff', expect.any(Buffer)); + }); +}); diff --git a/src/compare/libs/looks-same/looks-same.service.ts b/src/compare/libs/looks-same/looks-same.service.ts index d6d73c0b..0bab6d36 100644 --- a/src/compare/libs/looks-same/looks-same.service.ts +++ b/src/compare/libs/looks-same/looks-same.service.ts @@ -54,16 +54,15 @@ export class LookSameService implements ImageComparator { // compare const compareResult = await this.compare(baselineIgnored, imageIgnored, config); - result.pixelMisMatchCount = compareResult.differentPixels; - result.diffPercent = - compareResult.totalPixels > 0 ? (compareResult.differentPixels * 100) / compareResult.totalPixels : 0; - if (compareResult.equal) { result.status = TestStatus.ok; } else { result.status = TestStatus.unresolved; if (data.saveDiffAsFile) { - result.diffName = await this.staticService.saveImage('diff', await compareResult.diffImage.createBuffer('png')); + const diff = await this.createDiff(baselineIgnored, imageIgnored, config); + if (diff) { + Object.assign(result, diff); + } } } @@ -71,15 +70,35 @@ export class LookSameService implements ImageComparator { } async compare(baseline: PNG, image: PNG, config: LooksSameConfig): Promise { + const diffResult = await looksSame(PNG.sync.write(baseline), PNG.sync.write(image), config).catch((error) => { + this.logger.error(error.message); + }); + if (diffResult) { + return diffResult; + } + return undefined; + } + + async createDiff( + baseline: PNG, + image: PNG, + config: LooksSameConfig + ): Promise | undefined> { const diffResult = await looksSame(PNG.sync.write(baseline), PNG.sync.write(image), { ...config, createDiffImage: true, }).catch((error) => { this.logger.error(error.message); }); - if (diffResult) { - return diffResult; + + if (!diffResult || diffResult.equal) { + return undefined; } - return undefined; + + return { + diffName: await this.staticService.saveImage('diff', await diffResult.diffImage.createBuffer('png')), + diffPercent: diffResult.totalPixels > 0 ? (diffResult.differentPixels * 100) / diffResult.totalPixels : 0, + pixelMisMatchCount: diffResult.differentPixels, + }; } } diff --git a/src/compare/libs/looks-same/looks-same.types.ts b/src/compare/libs/looks-same/looks-same.types.ts index 47d77fd3..e25751fc 100644 --- a/src/compare/libs/looks-same/looks-same.types.ts +++ b/src/compare/libs/looks-same/looks-same.types.ts @@ -44,7 +44,7 @@ export interface CoordBounds { bottom: number; } -type LookSameBaseResult = { +export type LookSameResult = { /** * true if images are equal, false - otherwise */ @@ -57,29 +57,4 @@ type LookSameBaseResult = { * diff clusters for not equal images */ diffClusters?: CoordBounds[]; - /** - * number of pixels considered different - */ - differentPixels: number; - /** - * number of pixels compared - */ - totalPixels: number; - /** - * generated diff image when createDiffImage is enabled - */ -}; - -type DiffImage = { - createBuffer(extension: 'png' | 'raw'): Promise; }; - -export type LookSameResult = - | (LookSameBaseResult & { - equal: true; - diffImage: null; - }) - | (LookSameBaseResult & { - equal: false; - diffImage: DiffImage; - }); From de16b8b1bf8d73c3ed9679676dc3e7a2d625ebb2 Mon Sep 17 00:00:00 2001 From: Noah Lindner Date: Sat, 25 Jul 2026 21:51:02 -0400 Subject: [PATCH 4/4] refactor(looks-same): enforce saved diff contract --- .../libs/looks-same/looks-same.service.spec.ts | 9 +++++++++ src/compare/libs/looks-same/looks-same.service.ts | 14 ++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/compare/libs/looks-same/looks-same.service.spec.ts b/src/compare/libs/looks-same/looks-same.service.spec.ts index 203019dc..00b64263 100644 --- a/src/compare/libs/looks-same/looks-same.service.spec.ts +++ b/src/compare/libs/looks-same/looks-same.service.spec.ts @@ -227,4 +227,13 @@ describe('createDiff', () => { }); expect(saveImageMock).toHaveBeenCalledWith('diff', expect.any(Buffer)); }); + + it('rejects an equal result instead of silently omitting the artifact', async () => { + const image = new PNG({ width: 1, height: 1 }); + service = await initService({}); + + await expect(service.createDiff(image, image, DEFAULT_CONFIG)).rejects.toThrow( + 'Cannot create a diff image for an equal Looks-Same result' + ); + }); }); diff --git a/src/compare/libs/looks-same/looks-same.service.ts b/src/compare/libs/looks-same/looks-same.service.ts index 0bab6d36..e877f2d7 100644 --- a/src/compare/libs/looks-same/looks-same.service.ts +++ b/src/compare/libs/looks-same/looks-same.service.ts @@ -60,9 +60,9 @@ export class LookSameService implements ImageComparator { result.status = TestStatus.unresolved; if (data.saveDiffAsFile) { const diff = await this.createDiff(baselineIgnored, imageIgnored, config); - if (diff) { - Object.assign(result, diff); - } + result.diffName = diff.diffName; + result.diffPercent = diff.diffPercent; + result.pixelMisMatchCount = diff.pixelMisMatchCount; } } @@ -83,16 +83,14 @@ export class LookSameService implements ImageComparator { baseline: PNG, image: PNG, config: LooksSameConfig - ): Promise | undefined> { + ): Promise> { const diffResult = await looksSame(PNG.sync.write(baseline), PNG.sync.write(image), { ...config, createDiffImage: true, - }).catch((error) => { - this.logger.error(error.message); }); - if (!diffResult || diffResult.equal) { - return undefined; + if (diffResult.equal) { + throw new Error('Cannot create a diff image for an equal Looks-Same result'); } return {