vitest-plugin-vis 5.1.8 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/plugin.json +18 -0
- package/.cursor-plugin/plugin.json +18 -0
- package/dist/client/external/browser/image_data.mjs +18 -16
- package/dist/client/external/browser/image_data.mjs.map +1 -1
- package/dist/client/snapshot/compare_image_snapshot.mjs +31 -2
- package/dist/client/snapshot/compare_image_snapshot.mjs.map +1 -1
- package/dist/config/types.d.mts +2 -2
- package/dist/config/types.d.mts.map +1 -1
- package/dist/exports/client-api.d.mts +3 -3
- package/dist/exports/client-api.mjs +2 -2
- package/dist/exports/server-utils.d.mts +2 -2
- package/dist/exports/server-utils.mjs +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/server/vis_options.d.mts +4 -0
- package/dist/server/vis_server_context.logic.mjs +1 -1
- package/dist/server/vis_server_context.logic.mjs.map +1 -1
- package/dist/shared/changed_regions.mjs +143 -0
- package/dist/shared/changed_regions.mjs.map +1 -0
- package/dist/shared/compare_image.mjs +26 -1
- package/dist/shared/compare_image.mjs.map +1 -1
- package/dist/shared/constants.d.mts +3 -1
- package/dist/shared/constants.d.mts.map +1 -1
- package/dist/shared/constants.mjs +3 -1
- package/dist/shared/constants.mjs.map +1 -1
- package/dist/shared/review_image.mjs +160 -0
- package/dist/shared/review_image.mjs.map +1 -0
- package/dist/shared/types.d.mts +19 -4
- package/dist/shared/types.d.mts.map +1 -1
- package/llms.txt +54 -0
- package/package.json +8 -2
- package/plugin.json +24 -0
- package/skills/review-image-snapshot-failures/README.md +18 -0
- package/skills/review-image-snapshot-failures/SKILL.md +37 -0
- package/src/client/expect/to_match_image_snapshot.mdx +33 -0
- package/src/client/external/browser/image_data.ts +20 -19
- package/src/client/snapshot/compare_image_snapshot.ts +40 -2
- package/src/config/types.ts +3 -1
- package/src/config/vis.mdx +21 -0
- package/src/exports/server-utils.ts +7 -1
- package/src/overview.mdx +2 -0
- package/src/server/vis_server_context.logic.ts +1 -0
- package/src/shared/changed_regions.ts +163 -0
- package/src/shared/compare_image.ts +33 -1
- package/src/shared/constants.ts +2 -0
- package/src/shared/review_image.ts +180 -0
- package/src/shared/types.ts +21 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vitest-plugin-vis",
|
|
3
|
+
"version": "5.1.8",
|
|
4
|
+
"description": "Review failed image snapshots from vitest-plugin-vis cheaply: read the message, then the review image.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Homa Wong",
|
|
7
|
+
"email": "homawong@gmail.com",
|
|
8
|
+
"url": "https://github.com/unional"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/repobuddy/visual-testing/tree/main/packages/vitest-plugin-vis",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/repobuddy/visual-testing",
|
|
14
|
+
"directory": "packages/vitest-plugin-vis"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"keywords": ["image-snapshot", "vitest", "visual-test"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vitest-plugin-vis",
|
|
3
|
+
"version": "5.1.8",
|
|
4
|
+
"description": "Review failed image snapshots from vitest-plugin-vis cheaply: read the message, then the review image.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Homa Wong",
|
|
7
|
+
"email": "homawong@gmail.com",
|
|
8
|
+
"url": "https://github.com/unional"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/repobuddy/visual-testing/tree/main/packages/vitest-plugin-vis",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/repobuddy/visual-testing",
|
|
14
|
+
"directory": "packages/vitest-plugin-vis"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"keywords": ["image-snapshot", "vitest", "visual-test"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vitest-plugin-vis",
|
|
3
|
+
"version": "5.1.8",
|
|
4
|
+
"description": "Review failed image snapshots from vitest-plugin-vis cheaply: read the message, then the review image.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Homa Wong",
|
|
7
|
+
"email": "homawong@gmail.com",
|
|
8
|
+
"url": "https://github.com/unional"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/repobuddy/visual-testing/tree/main/packages/vitest-plugin-vis",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/repobuddy/visual-testing",
|
|
14
|
+
"directory": "packages/vitest-plugin-vis"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"keywords": ["image-snapshot", "vitest", "visual-test"]
|
|
18
|
+
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
//#region src/client/external/browser/image_data.ts
|
|
2
|
-
function toImageData(base64) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
2
|
+
async function toImageData(base64) {
|
|
3
|
+
let bitmap;
|
|
4
|
+
try {
|
|
5
|
+
const blob = await (await fetch(`data:image/png;base64,${base64}`)).blob();
|
|
6
|
+
bitmap = await createImageBitmap(blob);
|
|
7
|
+
} catch (cause) {
|
|
8
|
+
throw new Error("Failed to load image", { cause });
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const canvas = document.createElement("canvas");
|
|
12
|
+
canvas.width = bitmap.width;
|
|
13
|
+
canvas.height = bitmap.height;
|
|
14
|
+
const ctx = canvas.getContext("2d");
|
|
15
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
16
|
+
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
17
|
+
} finally {
|
|
18
|
+
bitmap.close();
|
|
19
|
+
}
|
|
18
20
|
}
|
|
19
21
|
async function toDataURL(imageData) {
|
|
20
22
|
const canvas = document.createElement("canvas");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image_data.mjs","names":[],"sources":["../../../../src/client/external/browser/image_data.ts"],"sourcesContent":["export function toImageData(base64: string) {\n\
|
|
1
|
+
{"version":3,"file":"image_data.mjs","names":[],"sources":["../../../../src/client/external/browser/image_data.ts"],"sourcesContent":["export async function toImageData(base64: string) {\n\t// `createImageBitmap` + `close()` releases the decoded pixels right away.\n\t// `new Image()` + `drawImage` leaves a full-size decode in Chromium's image decode cache.\n\tlet bitmap: ImageBitmap\n\ttry {\n\t\tconst blob = await (await fetch(`data:image/png;base64,${base64}`)).blob()\n\t\tbitmap = await createImageBitmap(blob)\n\t} catch (cause) {\n\t\tthrow new Error('Failed to load image', { cause })\n\t}\n\ttry {\n\t\tconst canvas = document.createElement('canvas')\n\t\tcanvas.width = bitmap.width\n\t\tcanvas.height = bitmap.height\n\t\tconst ctx = canvas.getContext('2d')!\n\t\tctx.drawImage(bitmap, 0, 0)\n\t\treturn ctx.getImageData(0, 0, canvas.width, canvas.height)\n\t} finally {\n\t\tbitmap.close()\n\t}\n}\nexport async function toDataURL(imageData: ImageData) {\n\tconst canvas = document.createElement('canvas')\n\tcanvas.width = imageData.width\n\tcanvas.height = imageData.height\n\tconst ctx = canvas.getContext('2d')!\n\tctx.putImageData(imageData, 0, 0)\n\treturn new Promise<string>((resolve) => {\n\t\tcanvas.toBlob((blob) => {\n\t\t\tconst reader = new FileReader()\n\t\t\treader.onload = () => {\n\t\t\t\tresolve(reader.result as string)\n\t\t\t}\n\t\t\treader.readAsDataURL(blob!)\n\t\t})\n\t})\n}\n"],"mappings":";AAAA,eAAsB,YAAY,QAAgB;CAGjD,IAAI;AACJ,KAAI;EACH,MAAM,OAAO,OAAO,MAAM,MAAM,yBAAyB,SAAS,EAAE,MAAM;AAC1E,WAAS,MAAM,kBAAkB,KAAK;UAC9B,OAAO;AACf,QAAM,IAAI,MAAM,wBAAwB,EAAE,OAAO,CAAC;;AAEnD,KAAI;EACH,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,SAAO,QAAQ,OAAO;AACtB,SAAO,SAAS,OAAO;EACvB,MAAM,MAAM,OAAO,WAAW,KAAK;AACnC,MAAI,UAAU,QAAQ,GAAG,EAAE;AAC3B,SAAO,IAAI,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,OAAO;WACjD;AACT,SAAO,OAAO;;;AAGhB,eAAsB,UAAU,WAAsB;CACrD,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,QAAQ,UAAU;AACzB,QAAO,SAAS,UAAU;AAE1B,CADY,OAAO,WAAW,KAAK,CAC/B,aAAa,WAAW,GAAG,EAAE;AACjC,QAAO,IAAI,SAAiB,YAAY;AACvC,SAAO,QAAQ,SAAS;GACvB,MAAM,SAAS,IAAI,YAAY;AAC/B,UAAO,eAAe;AACrB,YAAQ,OAAO,OAAiB;;AAEjC,UAAO,cAAc,KAAM;IAC1B;GACD"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CHANGED_REGIONS_SUFFIX, REVIEW_IMAGE_SUFFIX } from "../../shared/constants.mjs";
|
|
2
|
+
import { findChangedRegions, formatChangedRegions } from "../../shared/changed_regions.mjs";
|
|
3
|
+
import { compareImage, getChangedMask } from "../../shared/compare_image.mjs";
|
|
4
|
+
import { createReviewImage, getReviewImageSkipReason } from "../../shared/review_image.mjs";
|
|
2
5
|
import { toDataURL, toImageData } from "../external/browser/image_data.mjs";
|
|
3
6
|
import { server } from "../external/vitest/vitest_browser_context_proxy.mjs";
|
|
4
7
|
import { alignImagesToSameSize } from "../image/align_images.mjs";
|
|
@@ -43,6 +46,28 @@ async function compareImageSnapshot(commands, taskId, info, options) {
|
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
45
48
|
await writeSnapshot(commands, resolve(info.projectRoot, info.diffPath), diffImage);
|
|
49
|
+
const mask = getChangedMask(baselineAlignedImage.data, resultAlignedImage.data, diffImage.data, width, height, options);
|
|
50
|
+
const regions = findChangedRegions(mask, width, height);
|
|
51
|
+
await commands.writeFile(resolve(info.projectRoot, toSiblingPath(info.diffPath, CHANGED_REGIONS_SUFFIX)), JSON.stringify({
|
|
52
|
+
width,
|
|
53
|
+
height,
|
|
54
|
+
regions
|
|
55
|
+
}));
|
|
56
|
+
let review = "";
|
|
57
|
+
if (options?.reviewImage !== false) {
|
|
58
|
+
const skipReason = getReviewImageSkipReason({
|
|
59
|
+
baseline: baselineImage,
|
|
60
|
+
result: resultImage,
|
|
61
|
+
regions
|
|
62
|
+
});
|
|
63
|
+
if (skipReason) review = `\nReview: skipped, ${skipReason}; open Actual or Difference instead`;
|
|
64
|
+
else {
|
|
65
|
+
const reviewPath = toSiblingPath(info.diffPath, REVIEW_IMAGE_SUFFIX);
|
|
66
|
+
const reviewImage = createReviewImage(baselineAlignedImage, resultAlignedImage, mask, regions);
|
|
67
|
+
await writeSnapshot(commands, resolve(info.projectRoot, reviewPath), new ImageData(reviewImage.data, reviewImage.width, reviewImage.height));
|
|
68
|
+
review = `\nReview: ${formatSnapshotPath(info.projectRoot, reviewPath)} (baseline, result, diff of each region)`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
46
71
|
throw new Error(dedent`Snapshot \`${taskId}\` mismatched
|
|
47
72
|
|
|
48
73
|
${options?.failureThreshold ? options?.failureThresholdType === "percent" ? `Expected image to match within ${options.failureThreshold}% but was differ by ${diffAmount}%.` : `Expected image to match within ${options.failureThreshold} pixels but was differ by ${diffAmount} pixels.` : `Expected image to match but was differ by ${options?.failureThresholdType === "percent" ? `${diffAmount}%` : `${diffAmount} pixels`}.`}${baselineImage.width !== resultImage.width || baselineImage.height !== resultImage.height ? `\nThe image size changed from ${baselineImage.width}x${baselineImage.height} to ${resultImage.width}x${resultImage.height}.` : ""}
|
|
@@ -51,7 +76,11 @@ async function compareImageSnapshot(commands, taskId, info, options) {
|
|
|
51
76
|
|
|
52
77
|
Expected: ${formatSnapshotPath(info.projectRoot, info.baselinePath)}
|
|
53
78
|
Actual: ${formatSnapshotPath(info.projectRoot, info.resultPath)}
|
|
54
|
-
Difference: ${formatSnapshotPath(info.projectRoot, info.diffPath)}
|
|
79
|
+
Difference: ${formatSnapshotPath(info.projectRoot, info.diffPath)}
|
|
80
|
+
Changed: ${formatChangedRegions(regions)}${review}`);
|
|
81
|
+
}
|
|
82
|
+
function toSiblingPath(diffPath, suffix) {
|
|
83
|
+
return diffPath.replace(/\.png$/, suffix);
|
|
55
84
|
}
|
|
56
85
|
async function writeSnapshot(commands, path, image) {
|
|
57
86
|
const content = (await toDataURL(image)).split(",")[1];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compare_image_snapshot.mjs","names":[],"sources":["../../../src/client/snapshot/compare_image_snapshot.ts"],"sourcesContent":["import dedent from 'dedent'\nimport { resolve } from 'pathe'\nimport type { BrowserCommands } from 'vitest/browser'\nimport type {\n\tImageSnapshotNextIndexCommand,\n\tPrepareImageSnapshotComparisonCommand,\n} from '../../shared/commands.types.ts'\nimport { compareImage } from '../../shared/compare_image.ts'\nimport type { ImageSnapshotComparisonInfo, ToMatchImageSnapshotOptions } from '../../shared/types.ts'\nimport { toDataURL, toImageData } from '../external/browser/image_data.ts'\nimport { server } from '../external/vitest/vitest_browser_context_proxy.ts'\nimport { alignImagesToSameSize } from '../image/align_images.ts'\nimport { formatSnapshotPath, prettifyOptions } from './compare_image_snapshot.logic.ts'\n\nexport async function compareImageSnapshot(\n\tcommands: BrowserCommands & PrepareImageSnapshotComparisonCommand & ImageSnapshotNextIndexCommand,\n\ttaskId: string,\n\tinfo: ImageSnapshotComparisonInfo,\n\toptions?: ToMatchImageSnapshotOptions<any>,\n) {\n\toptions = { ...info, ...options } as any\n\n\tconst resultImage = await toImageData(info.result)\n\n\tif (!info.baseline) {\n\t\tif (server.config.snapshotOptions.updateSnapshot === 'all' || options?.createMissingBaseline === true) {\n\t\t\tawait writeSnapshot(commands, resolve(info.projectRoot, info.baselinePath), resultImage)\n\t\t\treturn\n\t\t}\n\t\tthrow new Error(\n\t\t\tdedent`Snapshot \\`${taskId}\\` has no baseline image. Please review the new snapshot and update the baseline image.\n\n\t\t\t\tOptions: ${prettifyOptions(options)}\n\t\t\t\tActual: ${formatSnapshotPath(info.projectRoot, info.resultPath)}`,\n\t\t)\n\t}\n\n\tconst baselineImage = await toImageData(info.baseline)\n\tconst [baselineAlignedImage, resultAlignedImage] = alignImagesToSameSize(baselineImage, resultImage)\n\tconst { width, height } = baselineAlignedImage\n\tconst diffImage = new ImageData(width, height)\n\tconst { pass, diffAmount } = compareImage(\n\t\tbaselineAlignedImage.data,\n\t\tresultAlignedImage.data,\n\t\tdiffImage.data,\n\t\twidth,\n\t\theight,\n\t\toptions as ToMatchImageSnapshotOptions<any>,\n\t)\n\tif (pass) {\n\t\tif (options?.expectToFail) {\n\t\t\tthrow new Error(\n\t\t\t\tdedent`Snapshot \\`${taskId}\\` matched but expected to fail.\n\n\t\t\t\t\t\t\tOptions: ${prettifyOptions(options)}\n\t\t\t\t\t\t\tDiff: ${options.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`}\n\n\t\t\t\t\t\t\tExpected: ${formatSnapshotPath(info.projectRoot, info.baselinePath)}\n\t\t\t\t\t\t\tActual: ${formatSnapshotPath(info.projectRoot, info.resultPath)}`,\n\t\t\t)\n\t\t}\n\t\treturn\n\t}\n\tif (server.config.snapshotOptions.updateSnapshot === 'all' && !options?.expectToFail) {\n\t\tawait writeSnapshot(commands, resolve(info.projectRoot, info.baselinePath), resultImage)\n\t\treturn\n\t}\n\n\tawait writeSnapshot(commands, resolve(info.projectRoot, info.diffPath), diffImage)\n\n\tthrow new Error(\n\t\tdedent`Snapshot \\`${taskId}\\` mismatched\n\n\t\t\t\t\t${\n\t\t\t\t\t\toptions?.failureThreshold\n\t\t\t\t\t\t\t? options?.failureThresholdType === 'percent'\n\t\t\t\t\t\t\t\t? `Expected image to match within ${options.failureThreshold}% but was differ by ${diffAmount}%.`\n\t\t\t\t\t\t\t\t: `Expected image to match within ${options.failureThreshold} pixels but was differ by ${diffAmount} pixels.`\n\t\t\t\t\t\t\t: `Expected image to match but was differ by ${options?.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`}.`\n\t\t\t\t\t}${\n\t\t\t\t\t\tbaselineImage.width !== resultImage.width || baselineImage.height !== resultImage.height\n\t\t\t\t\t\t\t? `\\nThe image size changed from ${baselineImage.width}x${baselineImage.height} to ${resultImage.width}x${resultImage.height}.`\n\t\t\t\t\t\t\t: ''\n\t\t\t\t\t}\n\n\t\t\t\t\tOptions: ${prettifyOptions(options)}\n\n\t\t\t\t\tExpected: ${formatSnapshotPath(info.projectRoot, info.baselinePath)}\n\t\t\t\t\tActual: ${formatSnapshotPath(info.projectRoot, info.resultPath)}\n\t\t\t\t\tDifference: ${formatSnapshotPath(info.projectRoot, info.diffPath)}`,\n\t)\n}\n\nasync function writeSnapshot(commands: BrowserCommands, path: string, image: ImageData) {\n\tconst content = (await toDataURL(image)).split(',')[1]!\n\treturn commands.writeFile(path, content, { encoding: 'base64' })\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"compare_image_snapshot.mjs","names":[],"sources":["../../../src/client/snapshot/compare_image_snapshot.ts"],"sourcesContent":["import dedent from 'dedent'\nimport { resolve } from 'pathe'\nimport type { BrowserCommands } from 'vitest/browser'\nimport { findChangedRegions, formatChangedRegions } from '../../shared/changed_regions.ts'\nimport type {\n\tImageSnapshotNextIndexCommand,\n\tPrepareImageSnapshotComparisonCommand,\n} from '../../shared/commands.types.ts'\nimport { compareImage, getChangedMask } from '../../shared/compare_image.ts'\nimport { CHANGED_REGIONS_SUFFIX, REVIEW_IMAGE_SUFFIX } from '../../shared/constants.ts'\nimport { createReviewImage, getReviewImageSkipReason } from '../../shared/review_image.ts'\nimport type { ImageSnapshotComparisonInfo, ToMatchImageSnapshotOptions } from '../../shared/types.ts'\nimport { toDataURL, toImageData } from '../external/browser/image_data.ts'\nimport { server } from '../external/vitest/vitest_browser_context_proxy.ts'\nimport { alignImagesToSameSize } from '../image/align_images.ts'\nimport { formatSnapshotPath, prettifyOptions } from './compare_image_snapshot.logic.ts'\n\nexport async function compareImageSnapshot(\n\tcommands: BrowserCommands & PrepareImageSnapshotComparisonCommand & ImageSnapshotNextIndexCommand,\n\ttaskId: string,\n\tinfo: ImageSnapshotComparisonInfo,\n\toptions?: ToMatchImageSnapshotOptions<any>,\n) {\n\toptions = { ...info, ...options } as any\n\n\tconst resultImage = await toImageData(info.result)\n\n\tif (!info.baseline) {\n\t\tif (server.config.snapshotOptions.updateSnapshot === 'all' || options?.createMissingBaseline === true) {\n\t\t\tawait writeSnapshot(commands, resolve(info.projectRoot, info.baselinePath), resultImage)\n\t\t\treturn\n\t\t}\n\t\tthrow new Error(\n\t\t\tdedent`Snapshot \\`${taskId}\\` has no baseline image. Please review the new snapshot and update the baseline image.\n\n\t\t\t\tOptions: ${prettifyOptions(options)}\n\t\t\t\tActual: ${formatSnapshotPath(info.projectRoot, info.resultPath)}`,\n\t\t)\n\t}\n\n\tconst baselineImage = await toImageData(info.baseline)\n\tconst [baselineAlignedImage, resultAlignedImage] = alignImagesToSameSize(baselineImage, resultImage)\n\tconst { width, height } = baselineAlignedImage\n\tconst diffImage = new ImageData(width, height)\n\tconst { pass, diffAmount } = compareImage(\n\t\tbaselineAlignedImage.data,\n\t\tresultAlignedImage.data,\n\t\tdiffImage.data,\n\t\twidth,\n\t\theight,\n\t\toptions as ToMatchImageSnapshotOptions<any>,\n\t)\n\tif (pass) {\n\t\tif (options?.expectToFail) {\n\t\t\tthrow new Error(\n\t\t\t\tdedent`Snapshot \\`${taskId}\\` matched but expected to fail.\n\n\t\t\t\t\t\t\tOptions: ${prettifyOptions(options)}\n\t\t\t\t\t\t\tDiff: ${options.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`}\n\n\t\t\t\t\t\t\tExpected: ${formatSnapshotPath(info.projectRoot, info.baselinePath)}\n\t\t\t\t\t\t\tActual: ${formatSnapshotPath(info.projectRoot, info.resultPath)}`,\n\t\t\t)\n\t\t}\n\t\treturn\n\t}\n\tif (server.config.snapshotOptions.updateSnapshot === 'all' && !options?.expectToFail) {\n\t\tawait writeSnapshot(commands, resolve(info.projectRoot, info.baselinePath), resultImage)\n\t\treturn\n\t}\n\n\tawait writeSnapshot(commands, resolve(info.projectRoot, info.diffPath), diffImage)\n\n\tconst mask = getChangedMask(\n\t\tbaselineAlignedImage.data,\n\t\tresultAlignedImage.data,\n\t\tdiffImage.data,\n\t\twidth,\n\t\theight,\n\t\toptions as ToMatchImageSnapshotOptions<any>,\n\t)\n\tconst regions = findChangedRegions(mask, width, height)\n\tawait commands.writeFile(\n\t\tresolve(info.projectRoot, toSiblingPath(info.diffPath, CHANGED_REGIONS_SUFFIX)),\n\t\tJSON.stringify({ width, height, regions }),\n\t)\n\tlet review = ''\n\tif (options?.reviewImage !== false) {\n\t\tconst skipReason = getReviewImageSkipReason({ baseline: baselineImage, result: resultImage, regions })\n\t\tif (skipReason) {\n\t\t\treview = `\\nReview: skipped, ${skipReason}; open Actual or Difference instead`\n\t\t} else {\n\t\t\tconst reviewPath = toSiblingPath(info.diffPath, REVIEW_IMAGE_SUFFIX)\n\t\t\tconst reviewImage = createReviewImage(baselineAlignedImage, resultAlignedImage, mask, regions)\n\t\t\tawait writeSnapshot(\n\t\t\t\tcommands,\n\t\t\t\tresolve(info.projectRoot, reviewPath),\n\t\t\t\tnew ImageData(reviewImage.data, reviewImage.width, reviewImage.height),\n\t\t\t)\n\t\t\treview = `\\nReview: ${formatSnapshotPath(info.projectRoot, reviewPath)} (baseline, result, diff of each region)`\n\t\t}\n\t}\n\n\tthrow new Error(\n\t\tdedent`Snapshot \\`${taskId}\\` mismatched\n\n\t\t\t\t\t${\n\t\t\t\t\t\toptions?.failureThreshold\n\t\t\t\t\t\t\t? options?.failureThresholdType === 'percent'\n\t\t\t\t\t\t\t\t? `Expected image to match within ${options.failureThreshold}% but was differ by ${diffAmount}%.`\n\t\t\t\t\t\t\t\t: `Expected image to match within ${options.failureThreshold} pixels but was differ by ${diffAmount} pixels.`\n\t\t\t\t\t\t\t: `Expected image to match but was differ by ${options?.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`}.`\n\t\t\t\t\t}${\n\t\t\t\t\t\tbaselineImage.width !== resultImage.width || baselineImage.height !== resultImage.height\n\t\t\t\t\t\t\t? `\\nThe image size changed from ${baselineImage.width}x${baselineImage.height} to ${resultImage.width}x${resultImage.height}.`\n\t\t\t\t\t\t\t: ''\n\t\t\t\t\t}\n\n\t\t\t\t\tOptions: ${prettifyOptions(options)}\n\n\t\t\t\t\tExpected: ${formatSnapshotPath(info.projectRoot, info.baselinePath)}\n\t\t\t\t\tActual: ${formatSnapshotPath(info.projectRoot, info.resultPath)}\n\t\t\t\t\tDifference: ${formatSnapshotPath(info.projectRoot, info.diffPath)}\n\t\t\t\t\tChanged: ${formatChangedRegions(regions)}${review}`,\n\t)\n}\n\nfunction toSiblingPath(diffPath: string, suffix: string) {\n\treturn diffPath.replace(/\\.png$/, suffix)\n}\n\nasync function writeSnapshot(commands: BrowserCommands, path: string, image: ImageData) {\n\tconst content = (await toDataURL(image)).split(',')[1]!\n\treturn commands.writeFile(path, content, { encoding: 'base64' })\n}\n"],"mappings":"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"}
|
package/dist/config/types.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ComparisonMethod, FailureThresholdOptions, ImageSnapshotKeyOptions, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, PixelComparisonOptions, PixelDiffOptions, SsimComparisonOptions, SsimDiffOptions } from "../shared/types.mjs";
|
|
1
|
+
import { ComparisonMethod, FailureThresholdOptions, ImageSnapshotKeyOptions, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, PixelComparisonOptions, PixelDiffOptions, ReviewImageOptions, SsimComparisonOptions, SsimDiffOptions } from "../shared/types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/config/types.d.ts
|
|
4
4
|
type VisOptions<M extends ComparisonMethod = 'pixel'> = (M extends 'ssim' ? {
|
|
@@ -13,7 +13,7 @@ type VisOptions<M extends ComparisonMethod = 'pixel'> = (M extends 'ssim' ? {
|
|
|
13
13
|
* Custom options passed to 'pixelmatch'
|
|
14
14
|
*/
|
|
15
15
|
diffOptions?: PixelDiffOptions | undefined;
|
|
16
|
-
}) & FailureThresholdOptions & ImageSnapshotTimeoutOptions & ImageSnapshotKeyOptions & ImageSnapshotSubjectOptions & {
|
|
16
|
+
}) & FailureThresholdOptions & ImageSnapshotTimeoutOptions & ImageSnapshotKeyOptions & ImageSnapshotSubjectOptions & ReviewImageOptions & {
|
|
17
17
|
/**
|
|
18
18
|
* Loads the `test.setupFiles` of the specified preset.
|
|
19
19
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../src/config/types.ts"],"sourcesContent":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../src/config/types.ts"],"sourcesContent":[],"mappings":";;;KAeY,qBAAqB,+BAA+B;EAApD,gBAAU,EAED,CAFC;EAAW;;;EAMR,WAAA,CAAA,EAAR,OAAQ,CAAA,eAAA,CAAA,GAAA,SAAA;CAAR,GAAA;EAGK,gBAAA,CAAA,EAAA,CAAA,GAAA,SAAA;EAIL;;;EAIhB,WAAA,CAAA,EAJgB,gBAIhB,GAAA,SAAA;CACA,CAAA,GAHA,uBAGA,GAFA,2BAEA,GADA,uBACA,GAAA,2BAAA,GACA,kBADA,GAAA;EACA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BASELINE_DIR, DIFF_DIR, NAME, RESULT_DIR, SNAPSHOT_ROOT_DIR } from "../shared/constants.mjs";
|
|
2
|
-
import { Awaitable, ComparisonMethod, FailureThresholdOptions, ImageSnapshotCompareOptions, ImageSnapshotComparisonInfo, ImageSnapshotKeyOptions, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, MatchImageSnapshotOptions, PageImageSnapshotOptions, PixelComparisonOptions, PixelDiffOptions, SetupVisOptions, SnapshotTestMeta, SsimComparisonOptions, SsimDiffOptions, ToMatchImageSnapshotOptions, ToMatchPageImageSnapshotOptions } from "../shared/types.mjs";
|
|
1
|
+
import { BASELINE_DIR, CHANGED_REGIONS_SUFFIX, DIFF_DIR, NAME, RESULT_DIR, REVIEW_IMAGE_SUFFIX, SNAPSHOT_ROOT_DIR } from "../shared/constants.mjs";
|
|
2
|
+
import { Awaitable, ComparisonMethod, FailureThresholdOptions, ImageSnapshotCompareOptions, ImageSnapshotComparisonInfo, ImageSnapshotKeyOptions, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, MatchImageSnapshotOptions, PageImageSnapshotOptions, PixelComparisonOptions, PixelDiffOptions, ReviewImageOptions, SetupVisOptions, SnapshotTestMeta, SsimComparisonOptions, SsimDiffOptions, ToMatchImageSnapshotOptions, ToMatchPageImageSnapshotOptions } from "../shared/types.mjs";
|
|
3
3
|
import "../server/commands/augment.mjs";
|
|
4
4
|
import { MetaTask, setAutoSnapshotOptions } from "../auto_snapshots/auto_snapshot_options.mjs";
|
|
5
5
|
import { hasImageSnapshotAction } from "../client/actions/has_image_snapshot_action.mjs";
|
|
@@ -10,4 +10,4 @@ import { success } from "../client/expect/expectation_result.mjs";
|
|
|
10
10
|
import { ImageSnapshotMatcher } from "../client/expect/to_match_image_snapshot.types.mjs";
|
|
11
11
|
import { autoSnapshotMatcher } from "../client/suite/auto_snapshot_matcher.mjs";
|
|
12
12
|
import { toTaskId } from "../client/task/task_id.mjs";
|
|
13
|
-
export { Awaitable, BASELINE_DIR, ComparisonMethod, DIFF_DIR, FailureThresholdOptions, ImageSnapshotCompareOptions, ImageSnapshotComparisonInfo, ImageSnapshotKeyOptions, ImageSnapshotMatcher, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, MatchImageSnapshotOptions, MetaTask, NAME, PageImageSnapshotOptions, PixelComparisonOptions, PixelDiffOptions, RESULT_DIR, SNAPSHOT_ROOT_DIR, SetupVisOptions, SnapshotTestMeta, SsimComparisonOptions, SsimDiffOptions, ToMatchImageSnapshotOptions, ToMatchPageImageSnapshotOptions, autoSnapshotMatcher, hasImageSnapshotAction, loadImageSnapshotResultsAction, matchImageSnapshotAction, matchPageImageSnapshotAction, setAutoSnapshotOptions, success, toTaskId };
|
|
13
|
+
export { Awaitable, BASELINE_DIR, CHANGED_REGIONS_SUFFIX, ComparisonMethod, DIFF_DIR, FailureThresholdOptions, ImageSnapshotCompareOptions, ImageSnapshotComparisonInfo, ImageSnapshotKeyOptions, ImageSnapshotMatcher, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, MatchImageSnapshotOptions, MetaTask, NAME, PageImageSnapshotOptions, PixelComparisonOptions, PixelDiffOptions, RESULT_DIR, REVIEW_IMAGE_SUFFIX, ReviewImageOptions, SNAPSHOT_ROOT_DIR, SetupVisOptions, SnapshotTestMeta, SsimComparisonOptions, SsimDiffOptions, ToMatchImageSnapshotOptions, ToMatchPageImageSnapshotOptions, autoSnapshotMatcher, hasImageSnapshotAction, loadImageSnapshotResultsAction, matchImageSnapshotAction, matchPageImageSnapshotAction, setAutoSnapshotOptions, success, toTaskId };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BASELINE_DIR, DIFF_DIR, NAME, RESULT_DIR, SNAPSHOT_ROOT_DIR } from "../shared/constants.mjs";
|
|
1
|
+
import { BASELINE_DIR, CHANGED_REGIONS_SUFFIX, DIFF_DIR, NAME, RESULT_DIR, REVIEW_IMAGE_SUFFIX, SNAPSHOT_ROOT_DIR } from "../shared/constants.mjs";
|
|
2
2
|
import { setAutoSnapshotOptions } from "../auto_snapshots/auto_snapshot_options.mjs";
|
|
3
3
|
import { hasImageSnapshotAction } from "../client/actions/has_image_snapshot_action.mjs";
|
|
4
4
|
import { loadImageSnapshotResultsAction } from "../client/actions/load_image_snapshot_results_action.mjs";
|
|
@@ -8,4 +8,4 @@ import { success } from "../client/expect/expectation_result.mjs";
|
|
|
8
8
|
import { toTaskId } from "../client/task/task_id.mjs";
|
|
9
9
|
import { autoSnapshotMatcher } from "../client/suite/auto_snapshot_matcher.mjs";
|
|
10
10
|
|
|
11
|
-
export { BASELINE_DIR, DIFF_DIR, NAME, RESULT_DIR, SNAPSHOT_ROOT_DIR, autoSnapshotMatcher, hasImageSnapshotAction, loadImageSnapshotResultsAction, matchImageSnapshotAction, matchPageImageSnapshotAction, setAutoSnapshotOptions, success, toTaskId };
|
|
11
|
+
export { BASELINE_DIR, CHANGED_REGIONS_SUFFIX, DIFF_DIR, NAME, RESULT_DIR, REVIEW_IMAGE_SUFFIX, SNAPSHOT_ROOT_DIR, autoSnapshotMatcher, hasImageSnapshotAction, loadImageSnapshotResultsAction, matchImageSnapshotAction, matchPageImageSnapshotAction, setAutoSnapshotOptions, success, toTaskId };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BASELINE_DIR, DIFF_DIR, SNAPSHOT_ROOT_DIR } from "../shared/constants.mjs";
|
|
1
|
+
import { BASELINE_DIR, CHANGED_REGIONS_SUFFIX, DIFF_DIR, REVIEW_IMAGE_SUFFIX, SNAPSHOT_ROOT_DIR } from "../shared/constants.mjs";
|
|
2
2
|
import { trimCommonFolder } from "../shared/trim_common_folder.mjs";
|
|
3
3
|
import { getSnapshotRootDir } from "../server/snapshot_path.mjs";
|
|
4
4
|
import { resolveLegacySnapshotFileRelativePath, resolveSnapshotSubpathWithinLimits } from "../server/snapshot_path_limits.mjs";
|
|
5
|
-
export { BASELINE_DIR, DIFF_DIR, SNAPSHOT_ROOT_DIR, getSnapshotRootDir, resolveLegacySnapshotFileRelativePath, resolveSnapshotSubpathWithinLimits, trimCommonFolder };
|
|
5
|
+
export { BASELINE_DIR, CHANGED_REGIONS_SUFFIX, DIFF_DIR, REVIEW_IMAGE_SUFFIX, SNAPSHOT_ROOT_DIR, getSnapshotRootDir, resolveLegacySnapshotFileRelativePath, resolveSnapshotSubpathWithinLimits, trimCommonFolder };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BASELINE_DIR, DIFF_DIR, SNAPSHOT_ROOT_DIR } from "../shared/constants.mjs";
|
|
1
|
+
import { BASELINE_DIR, CHANGED_REGIONS_SUFFIX, DIFF_DIR, REVIEW_IMAGE_SUFFIX, SNAPSHOT_ROOT_DIR } from "../shared/constants.mjs";
|
|
2
2
|
import { resolveLegacySnapshotFileRelativePath, resolveSnapshotSubpathWithinLimits } from "../server/snapshot_path_limits.mjs";
|
|
3
3
|
import { trimCommonFolder } from "../shared/trim_common_folder.mjs";
|
|
4
4
|
import { getSnapshotRootDir } from "../server/snapshot_path.mjs";
|
|
5
5
|
|
|
6
|
-
export { BASELINE_DIR, DIFF_DIR, SNAPSHOT_ROOT_DIR, getSnapshotRootDir, resolveLegacySnapshotFileRelativePath, resolveSnapshotSubpathWithinLimits, trimCommonFolder };
|
|
6
|
+
export { BASELINE_DIR, CHANGED_REGIONS_SUFFIX, DIFF_DIR, REVIEW_IMAGE_SUFFIX, SNAPSHOT_ROOT_DIR, getSnapshotRootDir, resolveLegacySnapshotFileRelativePath, resolveSnapshotSubpathWithinLimits, trimCommonFolder };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Awaitable, ComparisonMethod, FailureThresholdOptions, ImageSnapshotCompareOptions, ImageSnapshotComparisonInfo, ImageSnapshotKeyOptions, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, MatchImageSnapshotOptions, PageImageSnapshotOptions, PixelComparisonOptions, PixelDiffOptions, SetupVisOptions, SnapshotTestMeta, SsimComparisonOptions, SsimDiffOptions, ToMatchImageSnapshotOptions, ToMatchPageImageSnapshotOptions } from "./shared/types.mjs";
|
|
1
|
+
import { Awaitable, ComparisonMethod, FailureThresholdOptions, ImageSnapshotCompareOptions, ImageSnapshotComparisonInfo, ImageSnapshotKeyOptions, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, MatchImageSnapshotOptions, PageImageSnapshotOptions, PixelComparisonOptions, PixelDiffOptions, ReviewImageOptions, SetupVisOptions, SnapshotTestMeta, SsimComparisonOptions, SsimDiffOptions, ToMatchImageSnapshotOptions, ToMatchPageImageSnapshotOptions } from "./shared/types.mjs";
|
|
2
2
|
import { MetaTask, setAutoSnapshotOptions } from "./auto_snapshots/auto_snapshot_options.mjs";
|
|
3
3
|
import "./client/expect/augment.mjs";
|
|
4
4
|
import "./client/page/augment.mjs";
|
|
5
|
-
export { Awaitable, ComparisonMethod, FailureThresholdOptions, ImageSnapshotCompareOptions, ImageSnapshotComparisonInfo, ImageSnapshotKeyOptions, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, MatchImageSnapshotOptions, MetaTask, PageImageSnapshotOptions, PixelComparisonOptions, PixelDiffOptions, SetupVisOptions, SnapshotTestMeta, SsimComparisonOptions, SsimDiffOptions, ToMatchImageSnapshotOptions, ToMatchPageImageSnapshotOptions, setAutoSnapshotOptions };
|
|
5
|
+
export { Awaitable, ComparisonMethod, FailureThresholdOptions, ImageSnapshotCompareOptions, ImageSnapshotComparisonInfo, ImageSnapshotKeyOptions, ImageSnapshotSubjectOptions, ImageSnapshotTimeoutOptions, MatchImageSnapshotOptions, MetaTask, PageImageSnapshotOptions, PixelComparisonOptions, PixelDiffOptions, ReviewImageOptions, SetupVisOptions, SnapshotTestMeta, SsimComparisonOptions, SsimDiffOptions, ToMatchImageSnapshotOptions, ToMatchPageImageSnapshotOptions, setAutoSnapshotOptions };
|
|
@@ -15,6 +15,7 @@ declare function getVisOption(context: ExtendedBrowserCommandContext): {
|
|
|
15
15
|
timeout?: number | undefined;
|
|
16
16
|
snapshotKey?: string | undefined;
|
|
17
17
|
subject?: string | undefined;
|
|
18
|
+
reviewImage?: boolean | undefined;
|
|
18
19
|
preset?: "manual" | "auto" | "custom" | undefined;
|
|
19
20
|
snapshotRootDir?: string | ((context: {
|
|
20
21
|
ci: boolean;
|
|
@@ -47,6 +48,7 @@ declare function getVisOption(context: ExtendedBrowserCommandContext): {
|
|
|
47
48
|
timeout?: number | undefined;
|
|
48
49
|
snapshotKey?: string | undefined;
|
|
49
50
|
subject?: string | undefined;
|
|
51
|
+
reviewImage?: boolean | undefined;
|
|
50
52
|
preset?: "manual" | "auto" | "custom" | undefined;
|
|
51
53
|
snapshotRootDir?: string | ((context: {
|
|
52
54
|
ci: boolean;
|
|
@@ -79,6 +81,7 @@ declare function getVisOption(context: ExtendedBrowserCommandContext): {
|
|
|
79
81
|
timeout?: number | undefined;
|
|
80
82
|
snapshotKey?: string | undefined;
|
|
81
83
|
subject?: string | undefined;
|
|
84
|
+
reviewImage?: boolean | undefined;
|
|
82
85
|
preset?: "manual" | "auto" | "custom" | undefined;
|
|
83
86
|
snapshotRootDir?: string | ((context: {
|
|
84
87
|
ci: boolean;
|
|
@@ -102,6 +105,7 @@ declare function getVisOption(context: ExtendedBrowserCommandContext): {
|
|
|
102
105
|
timeout?: number | undefined;
|
|
103
106
|
snapshotKey?: string | undefined;
|
|
104
107
|
subject?: string | undefined;
|
|
108
|
+
reviewImage?: boolean | undefined;
|
|
105
109
|
preset?: "manual" | "auto" | "custom" | undefined;
|
|
106
110
|
snapshotRootDir?: string | ((context: {
|
|
107
111
|
ci: boolean;
|
|
@@ -42,7 +42,7 @@ function createVisServerContext() {
|
|
|
42
42
|
const resultPath = join(resultDir, snapshotFilename);
|
|
43
43
|
const diffPath = join(diffDir, snapshotFilename);
|
|
44
44
|
return {
|
|
45
|
-
...pick(getVisOption(browserContext), "comparisonMethod", "diffOptions", "failureThreshold", "failureThresholdType", "timeout"),
|
|
45
|
+
...pick(getVisOption(browserContext), "comparisonMethod", "diffOptions", "failureThreshold", "failureThresholdType", "reviewImage", "timeout"),
|
|
46
46
|
baselinePath,
|
|
47
47
|
resultPath,
|
|
48
48
|
diffPath
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vis_server_context.logic.mjs","names":[],"sources":["../../src/server/vis_server_context.logic.ts"],"sourcesContent":["import { basename, join, resolve } from 'pathe'\nimport { pick } from 'type-plus'\nimport type { ImageSnapshotKeyOptions } from '../exports/client-api.ts'\nimport type { ImageSnapshotResult } from '../shared/commands.types.ts'\nimport { file } from './externals/file.ts'\nimport {\n\tabsoluteSnapshotFilePath,\n\tgetLegacySnapshotFilename,\n\tresolveLegacySnapshotFileRelativePath,\n} from './snapshot_path_limits.ts'\nimport { getSuite, getTaskSubpath } from './suite.ts'\nimport { getVisOption } from './vis_options.ts'\nimport type { ExtendedBrowserCommandContext } from './vis_server_context.types.ts'\n\nexport function createVisServerContext() {\n\tconst context = {\n\t\tasync getSnapshotResults(browserContext: ExtendedBrowserCommandContext, taskId: string) {\n\t\t\tconst suiteInfo = await context.getSuiteInfo(browserContext, taskId)\n\t\t\tconst base = resolve(suiteInfo.projectRoot, suiteInfo.baselineDir)\n\n\t\t\tconst legacyPattern = join(base, `${suiteInfo.taskId}-*.png`)\n\t\t\tconst baselines = await file.glob(legacyPattern)\n\n\t\t\tconst resultPattern = join(resolve(suiteInfo.projectRoot, suiteInfo.resultDir), `${suiteInfo.taskId}-*.png`)\n\t\t\tconst results = await file.glob(resultPattern)\n\n\t\t\tconst diffPattern = join(resolve(suiteInfo.projectRoot, suiteInfo.diffDir), `${suiteInfo.taskId}-*.png`)\n\t\t\tconst diffs = await file.glob(diffPattern)\n\n\t\t\tconst r: ImageSnapshotResult[] = []\n\t\t\tawait Promise.all(\n\t\t\t\tbaselines.map(async (baselinePath) => {\n\t\t\t\t\tconst filename = basename(baselinePath)\n\t\t\t\t\tconst resultPath = results.find((p) => p.endsWith(filename))\n\t\t\t\t\tconst diffPath = diffs.find((p) => p.endsWith(filename))\n\n\t\t\t\t\tconst baselineBuffer = await file.tryReadFile(baselinePath)\n\t\t\t\t\tconst resultBuffer = await file.tryReadFile(resultPath)\n\t\t\t\t\tconst diffBuffer = await file.tryReadFile(diffPath)\n\t\t\t\t\tr.push({\n\t\t\t\t\t\tfilename,\n\t\t\t\t\t\tbaseline: baselineBuffer?.toString('base64'),\n\t\t\t\t\t\tresult: resultBuffer?.toString('base64'),\n\t\t\t\t\t\tdiff: diffBuffer?.toString('base64'),\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t)\n\t\t\treturn r\n\t\t},\n\t\tasync getSnapshotInfo(\n\t\t\tbrowserContext: ExtendedBrowserCommandContext,\n\t\t\ttaskId: string,\n\t\t\toptions?: ImageSnapshotKeyOptions,\n\t\t) {\n\t\t\tconst suiteInfo = await context.getSuiteInfo(browserContext, taskId)\n\t\t\tconst snapshotFilename = context.getSnapshotFilename(browserContext, suiteInfo, options?.snapshotKey)\n\n\t\t\tconst { baselineDir, resultDir, diffDir, task } = suiteInfo\n\n\t\t\ttask.count += 1\n\t\t\tconst baselinePath = join(baselineDir, snapshotFilename)\n\t\t\tconst resultPath = join(resultDir, snapshotFilename)\n\t\t\tconst diffPath = join(diffDir, snapshotFilename)\n\n\t\t\treturn {\n\t\t\t\t...pick(\n\t\t\t\t\tgetVisOption(browserContext),\n\t\t\t\t\t'comparisonMethod',\n\t\t\t\t\t'diffOptions',\n\t\t\t\t\t'failureThreshold',\n\t\t\t\t\t'failureThresholdType',\n\t\t\t\t\t'timeout',\n\t\t\t\t),\n\t\t\t\tbaselinePath,\n\t\t\t\tresultPath,\n\t\t\t\tdiffPath,\n\t\t\t}\n\t\t},\n\n\t\tasync getTaskCount(browserContext: ExtendedBrowserCommandContext, taskId: string) {\n\t\t\treturn (await context.getSuiteInfo(browserContext, taskId)).task.count\n\t\t},\n\t\tasync hasImageSnapshot(\n\t\t\tbrowserContext: ExtendedBrowserCommandContext,\n\t\t\ttaskId: string,\n\t\t\tsnapshotKey: string | undefined,\n\t\t) {\n\t\t\tconst info = await context.getSuiteInfo(browserContext, taskId)\n\t\t\tconst visOptions = getVisOption(browserContext)\n\t\t\tconst legacyFilename = getLegacySnapshotFilename({\n\t\t\t\ttaskId: info.taskId,\n\t\t\t\texplicitSnapshotKey: snapshotKey,\n\t\t\t\tdefaultSnapshotKey: typeof visOptions.snapshotKey === 'string' ? visOptions.snapshotKey : undefined,\n\t\t\t\ttaskCount: info.task.count,\n\t\t\t})\n\t\t\tconst filename = resolveLegacySnapshotFileRelativePath({\n\t\t\t\tprojectRoot: info.projectRoot,\n\t\t\t\tbaselineDir: info.baselineDir,\n\t\t\t\tlegacySnapshotFilename: legacyFilename,\n\t\t\t\tshortenLongSnapshotPaths: visOptions.shortenLongSnapshotPaths === true,\n\t\t\t})\n\t\t\tconst legacyAbs = absoluteSnapshotFilePath(info.projectRoot, info.baselineDir, filename)\n\t\t\treturn file.existFile(legacyAbs)\n\t\t},\n\t\tgetSnapshotFilename(\n\t\t\tbrowserContext: ExtendedBrowserCommandContext,\n\t\t\tinfo: { taskId: string; task: { count: number }; suiteId: string; baselineDir: string; projectRoot: string },\n\t\t\tsnapshotKey: string | undefined,\n\t\t) {\n\t\t\tconst visOptions = getVisOption(browserContext)\n\t\t\tconst legacyFilename = getLegacySnapshotFilename({\n\t\t\t\ttaskId: info.taskId,\n\t\t\t\texplicitSnapshotKey: snapshotKey,\n\t\t\t\tdefaultSnapshotKey: typeof visOptions.snapshotKey === 'string' ? visOptions.snapshotKey : undefined,\n\t\t\t\ttaskCount: info.task.count,\n\t\t\t})\n\t\t\treturn resolveLegacySnapshotFileRelativePath({\n\t\t\t\tprojectRoot: info.projectRoot,\n\t\t\t\tbaselineDir: info.baselineDir,\n\t\t\t\tlegacySnapshotFilename: legacyFilename,\n\t\t\t\tshortenLongSnapshotPaths: visOptions.shortenLongSnapshotPaths === true,\n\t\t\t})\n\t\t},\n\t\tasync getSuiteInfo(browserContext: ExtendedBrowserCommandContext, taskId: string) {\n\t\t\tconst projectState = await getSuite(browserContext)\n\t\t\tconst visOptions = getVisOption(browserContext)\n\t\t\tconst moduleId = getTaskSubpath(projectState, browserContext.testPath, visOptions)\n\t\t\tconst m = projectState.modules[moduleId]!\n\t\t\tconst task = (m.tasks[taskId] = m.tasks[taskId] ?? { count: 1 })\n\t\t\treturn {\n\t\t\t\tprojectRoot: projectState.projectRoot,\n\t\t\t\tsuiteId: moduleId,\n\t\t\t\ttaskId,\n\t\t\t\tbaselineDir: m.baselineDir,\n\t\t\t\tresultDir: m.resultDir,\n\t\t\t\tdiffDir: m.diffDir,\n\t\t\t\ttask,\n\t\t\t}\n\t\t},\n\t}\n\treturn context\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vis_server_context.logic.mjs","names":[],"sources":["../../src/server/vis_server_context.logic.ts"],"sourcesContent":["import { basename, join, resolve } from 'pathe'\nimport { pick } from 'type-plus'\nimport type { ImageSnapshotKeyOptions } from '../exports/client-api.ts'\nimport type { ImageSnapshotResult } from '../shared/commands.types.ts'\nimport { file } from './externals/file.ts'\nimport {\n\tabsoluteSnapshotFilePath,\n\tgetLegacySnapshotFilename,\n\tresolveLegacySnapshotFileRelativePath,\n} from './snapshot_path_limits.ts'\nimport { getSuite, getTaskSubpath } from './suite.ts'\nimport { getVisOption } from './vis_options.ts'\nimport type { ExtendedBrowserCommandContext } from './vis_server_context.types.ts'\n\nexport function createVisServerContext() {\n\tconst context = {\n\t\tasync getSnapshotResults(browserContext: ExtendedBrowserCommandContext, taskId: string) {\n\t\t\tconst suiteInfo = await context.getSuiteInfo(browserContext, taskId)\n\t\t\tconst base = resolve(suiteInfo.projectRoot, suiteInfo.baselineDir)\n\n\t\t\tconst legacyPattern = join(base, `${suiteInfo.taskId}-*.png`)\n\t\t\tconst baselines = await file.glob(legacyPattern)\n\n\t\t\tconst resultPattern = join(resolve(suiteInfo.projectRoot, suiteInfo.resultDir), `${suiteInfo.taskId}-*.png`)\n\t\t\tconst results = await file.glob(resultPattern)\n\n\t\t\tconst diffPattern = join(resolve(suiteInfo.projectRoot, suiteInfo.diffDir), `${suiteInfo.taskId}-*.png`)\n\t\t\tconst diffs = await file.glob(diffPattern)\n\n\t\t\tconst r: ImageSnapshotResult[] = []\n\t\t\tawait Promise.all(\n\t\t\t\tbaselines.map(async (baselinePath) => {\n\t\t\t\t\tconst filename = basename(baselinePath)\n\t\t\t\t\tconst resultPath = results.find((p) => p.endsWith(filename))\n\t\t\t\t\tconst diffPath = diffs.find((p) => p.endsWith(filename))\n\n\t\t\t\t\tconst baselineBuffer = await file.tryReadFile(baselinePath)\n\t\t\t\t\tconst resultBuffer = await file.tryReadFile(resultPath)\n\t\t\t\t\tconst diffBuffer = await file.tryReadFile(diffPath)\n\t\t\t\t\tr.push({\n\t\t\t\t\t\tfilename,\n\t\t\t\t\t\tbaseline: baselineBuffer?.toString('base64'),\n\t\t\t\t\t\tresult: resultBuffer?.toString('base64'),\n\t\t\t\t\t\tdiff: diffBuffer?.toString('base64'),\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t)\n\t\t\treturn r\n\t\t},\n\t\tasync getSnapshotInfo(\n\t\t\tbrowserContext: ExtendedBrowserCommandContext,\n\t\t\ttaskId: string,\n\t\t\toptions?: ImageSnapshotKeyOptions,\n\t\t) {\n\t\t\tconst suiteInfo = await context.getSuiteInfo(browserContext, taskId)\n\t\t\tconst snapshotFilename = context.getSnapshotFilename(browserContext, suiteInfo, options?.snapshotKey)\n\n\t\t\tconst { baselineDir, resultDir, diffDir, task } = suiteInfo\n\n\t\t\ttask.count += 1\n\t\t\tconst baselinePath = join(baselineDir, snapshotFilename)\n\t\t\tconst resultPath = join(resultDir, snapshotFilename)\n\t\t\tconst diffPath = join(diffDir, snapshotFilename)\n\n\t\t\treturn {\n\t\t\t\t...pick(\n\t\t\t\t\tgetVisOption(browserContext),\n\t\t\t\t\t'comparisonMethod',\n\t\t\t\t\t'diffOptions',\n\t\t\t\t\t'failureThreshold',\n\t\t\t\t\t'failureThresholdType',\n\t\t\t\t\t'reviewImage',\n\t\t\t\t\t'timeout',\n\t\t\t\t),\n\t\t\t\tbaselinePath,\n\t\t\t\tresultPath,\n\t\t\t\tdiffPath,\n\t\t\t}\n\t\t},\n\n\t\tasync getTaskCount(browserContext: ExtendedBrowserCommandContext, taskId: string) {\n\t\t\treturn (await context.getSuiteInfo(browserContext, taskId)).task.count\n\t\t},\n\t\tasync hasImageSnapshot(\n\t\t\tbrowserContext: ExtendedBrowserCommandContext,\n\t\t\ttaskId: string,\n\t\t\tsnapshotKey: string | undefined,\n\t\t) {\n\t\t\tconst info = await context.getSuiteInfo(browserContext, taskId)\n\t\t\tconst visOptions = getVisOption(browserContext)\n\t\t\tconst legacyFilename = getLegacySnapshotFilename({\n\t\t\t\ttaskId: info.taskId,\n\t\t\t\texplicitSnapshotKey: snapshotKey,\n\t\t\t\tdefaultSnapshotKey: typeof visOptions.snapshotKey === 'string' ? visOptions.snapshotKey : undefined,\n\t\t\t\ttaskCount: info.task.count,\n\t\t\t})\n\t\t\tconst filename = resolveLegacySnapshotFileRelativePath({\n\t\t\t\tprojectRoot: info.projectRoot,\n\t\t\t\tbaselineDir: info.baselineDir,\n\t\t\t\tlegacySnapshotFilename: legacyFilename,\n\t\t\t\tshortenLongSnapshotPaths: visOptions.shortenLongSnapshotPaths === true,\n\t\t\t})\n\t\t\tconst legacyAbs = absoluteSnapshotFilePath(info.projectRoot, info.baselineDir, filename)\n\t\t\treturn file.existFile(legacyAbs)\n\t\t},\n\t\tgetSnapshotFilename(\n\t\t\tbrowserContext: ExtendedBrowserCommandContext,\n\t\t\tinfo: { taskId: string; task: { count: number }; suiteId: string; baselineDir: string; projectRoot: string },\n\t\t\tsnapshotKey: string | undefined,\n\t\t) {\n\t\t\tconst visOptions = getVisOption(browserContext)\n\t\t\tconst legacyFilename = getLegacySnapshotFilename({\n\t\t\t\ttaskId: info.taskId,\n\t\t\t\texplicitSnapshotKey: snapshotKey,\n\t\t\t\tdefaultSnapshotKey: typeof visOptions.snapshotKey === 'string' ? visOptions.snapshotKey : undefined,\n\t\t\t\ttaskCount: info.task.count,\n\t\t\t})\n\t\t\treturn resolveLegacySnapshotFileRelativePath({\n\t\t\t\tprojectRoot: info.projectRoot,\n\t\t\t\tbaselineDir: info.baselineDir,\n\t\t\t\tlegacySnapshotFilename: legacyFilename,\n\t\t\t\tshortenLongSnapshotPaths: visOptions.shortenLongSnapshotPaths === true,\n\t\t\t})\n\t\t},\n\t\tasync getSuiteInfo(browserContext: ExtendedBrowserCommandContext, taskId: string) {\n\t\t\tconst projectState = await getSuite(browserContext)\n\t\t\tconst visOptions = getVisOption(browserContext)\n\t\t\tconst moduleId = getTaskSubpath(projectState, browserContext.testPath, visOptions)\n\t\t\tconst m = projectState.modules[moduleId]!\n\t\t\tconst task = (m.tasks[taskId] = m.tasks[taskId] ?? { count: 1 })\n\t\t\treturn {\n\t\t\t\tprojectRoot: projectState.projectRoot,\n\t\t\t\tsuiteId: moduleId,\n\t\t\t\ttaskId,\n\t\t\t\tbaselineDir: m.baselineDir,\n\t\t\t\tresultDir: m.resultDir,\n\t\t\t\tdiffDir: m.diffDir,\n\t\t\t\ttask,\n\t\t\t}\n\t\t},\n\t}\n\treturn context\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
//#region src/shared/changed_regions.ts
|
|
2
|
+
/**
|
|
3
|
+
* How far, in pixels, the changed-pixel mask grows before it is split into regions.
|
|
4
|
+
* Changes closer than twice this distance end up in the same region.
|
|
5
|
+
*/
|
|
6
|
+
const CHANGED_REGION_MARGIN = 16;
|
|
7
|
+
/**
|
|
8
|
+
* Finds the areas of an image that changed.
|
|
9
|
+
*
|
|
10
|
+
* The changed-pixel mask grows by `margin` pixels in every direction,
|
|
11
|
+
* and each connected area becomes one box.
|
|
12
|
+
* Boxes that overlap are merged.
|
|
13
|
+
*
|
|
14
|
+
* @param mask One byte per pixel; non-zero marks a changed pixel.
|
|
15
|
+
* @returns The boxes, sorted top to bottom, then left to right.
|
|
16
|
+
*/
|
|
17
|
+
function findChangedRegions(mask, width, height, margin = CHANGED_REGION_MARGIN) {
|
|
18
|
+
return mergeOverlappingRegions(labelRegions(growMask(mask, width, height, margin), width, height));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Grows the mask by `margin` pixels in every direction (a square dilation),
|
|
22
|
+
* in two passes so the cost does not depend on the margin.
|
|
23
|
+
*/
|
|
24
|
+
function growMask(mask, width, height, margin) {
|
|
25
|
+
const horizontal = new Uint8Array(width * height);
|
|
26
|
+
const rowCounts = new Int32Array(width + 1);
|
|
27
|
+
for (let y = 0; y < height; y++) {
|
|
28
|
+
const row = y * width;
|
|
29
|
+
for (let x = 0; x < width; x++) rowCounts[x + 1] = rowCounts[x] + (mask[row + x] ? 1 : 0);
|
|
30
|
+
if (rowCounts[width] === 0) continue;
|
|
31
|
+
for (let x = 0; x < width; x++) {
|
|
32
|
+
const from = Math.max(0, x - margin);
|
|
33
|
+
if (rowCounts[Math.min(width, x + margin + 1)] - rowCounts[from] > 0) horizontal[row + x] = 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const grown = new Uint8Array(width * height);
|
|
37
|
+
const columnCounts = new Int32Array(height + 1);
|
|
38
|
+
for (let x = 0; x < width; x++) {
|
|
39
|
+
for (let y = 0; y < height; y++) columnCounts[y + 1] = columnCounts[y] + horizontal[y * width + x];
|
|
40
|
+
if (columnCounts[height] === 0) continue;
|
|
41
|
+
for (let y = 0; y < height; y++) {
|
|
42
|
+
const from = Math.max(0, y - margin);
|
|
43
|
+
if (columnCounts[Math.min(height, y + margin + 1)] - columnCounts[from] > 0) grown[y * width + x] = 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return grown;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns the bounding box of each 4-connected area of the mask.
|
|
50
|
+
*/
|
|
51
|
+
function labelRegions(mask, width, height) {
|
|
52
|
+
const visited = new Uint8Array(width * height);
|
|
53
|
+
const stack = new Int32Array(width * height);
|
|
54
|
+
const regions = [];
|
|
55
|
+
for (let start = 0; start < mask.length; start++) {
|
|
56
|
+
if (!mask[start] || visited[start]) continue;
|
|
57
|
+
let minX = width;
|
|
58
|
+
let minY = height;
|
|
59
|
+
let maxX = -1;
|
|
60
|
+
let maxY = -1;
|
|
61
|
+
let top = 0;
|
|
62
|
+
stack[top++] = start;
|
|
63
|
+
visited[start] = 1;
|
|
64
|
+
while (top > 0) {
|
|
65
|
+
const i = stack[--top];
|
|
66
|
+
const x = i % width;
|
|
67
|
+
const y = (i - x) / width;
|
|
68
|
+
if (x < minX) minX = x;
|
|
69
|
+
if (x > maxX) maxX = x;
|
|
70
|
+
if (y < minY) minY = y;
|
|
71
|
+
if (y > maxY) maxY = y;
|
|
72
|
+
if (x > 0 && mask[i - 1] && !visited[i - 1]) {
|
|
73
|
+
visited[i - 1] = 1;
|
|
74
|
+
stack[top++] = i - 1;
|
|
75
|
+
}
|
|
76
|
+
if (x < width - 1 && mask[i + 1] && !visited[i + 1]) {
|
|
77
|
+
visited[i + 1] = 1;
|
|
78
|
+
stack[top++] = i + 1;
|
|
79
|
+
}
|
|
80
|
+
if (y > 0 && mask[i - width] && !visited[i - width]) {
|
|
81
|
+
visited[i - width] = 1;
|
|
82
|
+
stack[top++] = i - width;
|
|
83
|
+
}
|
|
84
|
+
if (y < height - 1 && mask[i + width] && !visited[i + width]) {
|
|
85
|
+
visited[i + width] = 1;
|
|
86
|
+
stack[top++] = i + width;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
regions.push({
|
|
90
|
+
x: minX,
|
|
91
|
+
y: minY,
|
|
92
|
+
width: maxX - minX + 1,
|
|
93
|
+
height: maxY - minY + 1
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return regions;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Merges boxes that overlap until no two boxes overlap.
|
|
100
|
+
*
|
|
101
|
+
* @returns The boxes, sorted top to bottom, then left to right.
|
|
102
|
+
*/
|
|
103
|
+
function mergeOverlappingRegions(regions) {
|
|
104
|
+
const result = regions.map((r) => ({ ...r }));
|
|
105
|
+
let merged = true;
|
|
106
|
+
while (merged) {
|
|
107
|
+
merged = false;
|
|
108
|
+
for (let i = 0; i < result.length; i++) for (let j = i + 1; j < result.length; j++) {
|
|
109
|
+
const a = result[i];
|
|
110
|
+
const b = result[j];
|
|
111
|
+
if (!overlaps(a, b)) continue;
|
|
112
|
+
const x = Math.min(a.x, b.x);
|
|
113
|
+
const y = Math.min(a.y, b.y);
|
|
114
|
+
result[i] = {
|
|
115
|
+
x,
|
|
116
|
+
y,
|
|
117
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
118
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y
|
|
119
|
+
};
|
|
120
|
+
result.splice(j, 1);
|
|
121
|
+
j = i;
|
|
122
|
+
merged = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
126
|
+
}
|
|
127
|
+
function overlaps(a, b) {
|
|
128
|
+
return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Describes the regions for a failure message,
|
|
132
|
+
* e.g. `2 regions: 106x37 at (0,0), 40x20 at (300,512)`.
|
|
133
|
+
*/
|
|
134
|
+
function formatChangedRegions(regions, maxListed = 5) {
|
|
135
|
+
if (regions.length === 0) return "no region found";
|
|
136
|
+
const listed = regions.slice(0, maxListed).map((r) => `${r.width}x${r.height} at (${r.x},${r.y})`).join(", ");
|
|
137
|
+
const more = regions.length > maxListed ? `, and ${regions.length - maxListed} more` : "";
|
|
138
|
+
return `${regions.length} ${regions.length === 1 ? "region" : "regions"}: ${listed}${more}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { findChangedRegions, formatChangedRegions };
|
|
143
|
+
//# sourceMappingURL=changed_regions.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changed_regions.mjs","names":[],"sources":["../../src/shared/changed_regions.ts"],"sourcesContent":["/**\n * A rectangular area of an image, in pixels.\n */\nexport type ChangedRegion = {\n\tx: number\n\ty: number\n\twidth: number\n\theight: number\n}\n\n/**\n * How far, in pixels, the changed-pixel mask grows before it is split into regions.\n * Changes closer than twice this distance end up in the same region.\n */\nexport const CHANGED_REGION_MARGIN = 16\n\n/**\n * Finds the areas of an image that changed.\n *\n * The changed-pixel mask grows by `margin` pixels in every direction,\n * and each connected area becomes one box.\n * Boxes that overlap are merged.\n *\n * @param mask One byte per pixel; non-zero marks a changed pixel.\n * @returns The boxes, sorted top to bottom, then left to right.\n */\nexport function findChangedRegions(\n\tmask: ArrayLike<number>,\n\twidth: number,\n\theight: number,\n\tmargin = CHANGED_REGION_MARGIN,\n): ChangedRegion[] {\n\tconst grown = growMask(mask, width, height, margin)\n\treturn mergeOverlappingRegions(labelRegions(grown, width, height))\n}\n\n/**\n * Grows the mask by `margin` pixels in every direction (a square dilation),\n * in two passes so the cost does not depend on the margin.\n */\nfunction growMask(mask: ArrayLike<number>, width: number, height: number, margin: number) {\n\tconst horizontal = new Uint8Array(width * height)\n\tconst rowCounts = new Int32Array(width + 1)\n\tfor (let y = 0; y < height; y++) {\n\t\tconst row = y * width\n\t\tfor (let x = 0; x < width; x++) rowCounts[x + 1] = rowCounts[x]! + (mask[row + x] ? 1 : 0)\n\t\tif (rowCounts[width] === 0) continue\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tconst from = Math.max(0, x - margin)\n\t\t\tconst to = Math.min(width, x + margin + 1)\n\t\t\tif (rowCounts[to]! - rowCounts[from]! > 0) horizontal[row + x] = 1\n\t\t}\n\t}\n\n\tconst grown = new Uint8Array(width * height)\n\tconst columnCounts = new Int32Array(height + 1)\n\tfor (let x = 0; x < width; x++) {\n\t\tfor (let y = 0; y < height; y++) columnCounts[y + 1] = columnCounts[y]! + horizontal[y * width + x]!\n\t\tif (columnCounts[height] === 0) continue\n\t\tfor (let y = 0; y < height; y++) {\n\t\t\tconst from = Math.max(0, y - margin)\n\t\t\tconst to = Math.min(height, y + margin + 1)\n\t\t\tif (columnCounts[to]! - columnCounts[from]! > 0) grown[y * width + x] = 1\n\t\t}\n\t}\n\treturn grown\n}\n\n/**\n * Returns the bounding box of each 4-connected area of the mask.\n */\nfunction labelRegions(mask: Uint8Array, width: number, height: number) {\n\tconst visited = new Uint8Array(width * height)\n\tconst stack = new Int32Array(width * height)\n\tconst regions: ChangedRegion[] = []\n\tfor (let start = 0; start < mask.length; start++) {\n\t\tif (!mask[start] || visited[start]) continue\n\t\tlet minX = width\n\t\tlet minY = height\n\t\tlet maxX = -1\n\t\tlet maxY = -1\n\t\tlet top = 0\n\t\tstack[top++] = start\n\t\tvisited[start] = 1\n\t\twhile (top > 0) {\n\t\t\tconst i = stack[--top]!\n\t\t\tconst x = i % width\n\t\t\tconst y = (i - x) / width\n\t\t\tif (x < minX) minX = x\n\t\t\tif (x > maxX) maxX = x\n\t\t\tif (y < minY) minY = y\n\t\t\tif (y > maxY) maxY = y\n\t\t\tif (x > 0 && mask[i - 1] && !visited[i - 1]) {\n\t\t\t\tvisited[i - 1] = 1\n\t\t\t\tstack[top++] = i - 1\n\t\t\t}\n\t\t\tif (x < width - 1 && mask[i + 1] && !visited[i + 1]) {\n\t\t\t\tvisited[i + 1] = 1\n\t\t\t\tstack[top++] = i + 1\n\t\t\t}\n\t\t\tif (y > 0 && mask[i - width] && !visited[i - width]) {\n\t\t\t\tvisited[i - width] = 1\n\t\t\t\tstack[top++] = i - width\n\t\t\t}\n\t\t\tif (y < height - 1 && mask[i + width] && !visited[i + width]) {\n\t\t\t\tvisited[i + width] = 1\n\t\t\t\tstack[top++] = i + width\n\t\t\t}\n\t\t}\n\t\tregions.push({ x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 })\n\t}\n\treturn regions\n}\n\n/**\n * Merges boxes that overlap until no two boxes overlap.\n *\n * @returns The boxes, sorted top to bottom, then left to right.\n */\nexport function mergeOverlappingRegions(regions: ChangedRegion[]): ChangedRegion[] {\n\tconst result = regions.map((r) => ({ ...r }))\n\tlet merged = true\n\twhile (merged) {\n\t\tmerged = false\n\t\tfor (let i = 0; i < result.length; i++) {\n\t\t\tfor (let j = i + 1; j < result.length; j++) {\n\t\t\t\tconst a = result[i]!\n\t\t\t\tconst b = result[j]!\n\t\t\t\tif (!overlaps(a, b)) continue\n\t\t\t\tconst x = Math.min(a.x, b.x)\n\t\t\t\tconst y = Math.min(a.y, b.y)\n\t\t\t\tresult[i] = {\n\t\t\t\t\tx,\n\t\t\t\t\ty,\n\t\t\t\t\twidth: Math.max(a.x + a.width, b.x + b.width) - x,\n\t\t\t\t\theight: Math.max(a.y + a.height, b.y + b.height) - y,\n\t\t\t\t}\n\t\t\t\tresult.splice(j, 1)\n\t\t\t\tj = i\n\t\t\t\tmerged = true\n\t\t\t}\n\t\t}\n\t}\n\treturn result.sort((a, b) => a.y - b.y || a.x - b.x)\n}\n\nfunction overlaps(a: ChangedRegion, b: ChangedRegion) {\n\treturn a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height\n}\n\n/**\n * Describes the regions for a failure message,\n * e.g. `2 regions: 106x37 at (0,0), 40x20 at (300,512)`.\n */\nexport function formatChangedRegions(regions: ChangedRegion[], maxListed = 5) {\n\tif (regions.length === 0) return 'no region found'\n\tconst listed = regions\n\t\t.slice(0, maxListed)\n\t\t.map((r) => `${r.width}x${r.height} at (${r.x},${r.y})`)\n\t\t.join(', ')\n\tconst more = regions.length > maxListed ? `, and ${regions.length - maxListed} more` : ''\n\treturn `${regions.length} ${regions.length === 1 ? 'region' : 'regions'}: ${listed}${more}`\n}\n"],"mappings":"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"}
|
|
@@ -16,6 +16,31 @@ function compareImage(img1, img2, output, width, height, options = {}) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
+
* A pixel counts as changed in the SSIM comparison when its SSIM value is below this.
|
|
20
|
+
*/
|
|
21
|
+
const SSIM_CHANGED_THRESHOLD = .95;
|
|
22
|
+
/**
|
|
23
|
+
* Returns one byte per pixel, non-zero where the pixel changed.
|
|
24
|
+
*
|
|
25
|
+
* The pixel method runs pixelmatch again in mask mode, so anti-aliased pixels are not included.
|
|
26
|
+
* The SSIM method reads the diff written by {@link compareImage} and applies {@link SSIM_CHANGED_THRESHOLD}.
|
|
27
|
+
*/
|
|
28
|
+
function getChangedMask(img1, img2, diff, width, height, options = {}) {
|
|
29
|
+
const mask = new Uint8Array(width * height);
|
|
30
|
+
if (options.comparisonMethod === "ssim") {
|
|
31
|
+
const minAlpha = 255 * (1 - SSIM_CHANGED_THRESHOLD);
|
|
32
|
+
for (let i = 0; i < mask.length; i++) if (diff[i * 4 + 3] > minAlpha) mask[i] = 1;
|
|
33
|
+
return mask;
|
|
34
|
+
}
|
|
35
|
+
const output = new Uint8ClampedArray(width * height * 4);
|
|
36
|
+
pixelMatch(img1, img2, output, width, height, {
|
|
37
|
+
...options.diffOptions,
|
|
38
|
+
diffMask: true
|
|
39
|
+
});
|
|
40
|
+
for (let i = 0; i < mask.length; i++) if (output[i * 4 + 3]) mask[i] = 1;
|
|
41
|
+
return mask;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
19
44
|
* @author This code is based on `jest-image-snapshot` implementation.
|
|
20
45
|
*/
|
|
21
46
|
function compareWithSsim(img1, img2, output, width, height, diffOptions) {
|
|
@@ -42,5 +67,5 @@ function compareWithSsim(img1, img2, output, width, height, diffOptions) {
|
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
//#endregion
|
|
45
|
-
export { compareImage };
|
|
70
|
+
export { compareImage, getChangedMask };
|
|
46
71
|
//# sourceMappingURL=compare_image.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compare_image.mjs","names":["pixelmatch"],"sources":["../../src/shared/compare_image.ts"],"sourcesContent":["import pixelmatch from 'pixelmatch'\nimport { type Options, ssim } from 'ssim.js'\nimport { convertThresholdUnit } from './convert_threshold_unit.ts'\nimport type { ComparisonMethod, ImageSnapshotCompareOptions } from './types.ts'\n\nexport function compareImage<M extends ComparisonMethod>(\n\timg1: Uint8ClampedArray | Buffer,\n\timg2: Uint8ClampedArray | Buffer,\n\toutput: Uint8ClampedArray | Buffer,\n\twidth: number,\n\theight: number,\n\toptions: ImageSnapshotCompareOptions<M> = {} as any,\n) {\n\tconst pixelDiff =\n\t\toptions.comparisonMethod === 'ssim'\n\t\t\t? compareWithSsim(img1, img2, output, width, height, options.diffOptions)\n\t\t\t: pixelmatch(img1, img2, output, width, height, options.diffOptions)\n\tconst diffAmount = convertThresholdUnit(\n\t\t{ failureThresholdType: options.failureThresholdType ?? 'pixel', width, height },\n\t\tpixelDiff,\n\t)\n\n\treturn {\n\t\tpass: diffAmount <= (options.failureThreshold ?? 0),\n\t\tdiffAmount,\n\t}\n}\n\n/**\n * @author This code is based on `jest-image-snapshot` implementation.\n */\nfunction compareWithSsim(\n\timg1: Uint8ClampedArray | Buffer,\n\timg2: Uint8ClampedArray | Buffer,\n\toutput: Uint8ClampedArray | Buffer,\n\twidth: number,\n\theight: number,\n\tdiffOptions?: Partial<Options>,\n) {\n\tconst newImage = { data: img1, width: width, height: height }\n\tconst baselineImage = { data: img2, width: width, height: height }\n\tconst { ssim_map, mssim } = ssim(newImage as any, baselineImage as any, {\n\t\tssim: 'bezkrovny',\n\t\t...diffOptions,\n\t})\n\t// Converts the SSIM value to different pixels based on image width and height\n\t// conforms to how pixelmatch works.\n\tconst diffPixels = Math.round((1 - mssim) * width * height)\n\tconst diffRgbaPixels = new DataView(output.buffer, output.byteOffset)\n\tfor (let ln = 0; ln !== height; ++ln) {\n\t\tfor (let pos = 0; pos !== width; ++pos) {\n\t\t\tconst rPos = ln * width + pos\n\t\t\t// initial value is transparent. We'll add in the SSIM offset.\n\t\t\t// red (ff) green (00) blue (00) alpha (00)\n\t\t\tconst diffValue =\n\t\t\t\t0xff000000 +\n\t\t\t\tMath.floor(\n\t\t\t\t\t0xff *\n\t\t\t\t\t\t(1 -\n\t\t\t\t\t\t\tssim_map.data[\n\t\t\t\t\t\t\t\tssim_map.width * Math.round((ssim_map.height * ln) / height) +\n\t\t\t\t\t\t\t\t\tMath.round((ssim_map.width * pos) / width)\n\t\t\t\t\t\t\t]!),\n\t\t\t\t)\n\t\t\tdiffRgbaPixels.setUint32(rPos * 4, diffValue)\n\t\t}\n\t}\n\treturn diffPixels\n}\n"],"mappings":";;;;;AAKA,SAAgB,aACf,MACA,MACA,QACA,OACA,QACA,UAA0C,EAAE,EAC3C;CACD,MAAM,YACL,QAAQ,qBAAqB,SAC1B,gBAAgB,MAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,YAAY,GACvEA,WAAW,MAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,YAAY;CACtE,MAAM,aAAa,qBAClB;EAAE,sBAAsB,QAAQ,wBAAwB;EAAS;EAAO;EAAQ,EAChF,UACA;AAED,QAAO;EACN,MAAM,eAAe,QAAQ,oBAAoB;EACjD;EACA;;;;;AAMF,SAAS,gBACR,MACA,MACA,QACA,OACA,QACA,aACC;CAGD,MAAM,EAAE,UAAU,UAAU,KAFX;EAAE,MAAM;EAAa;EAAe;EAAQ,EACvC;EAAE,MAAM;EAAa;EAAe;EAAQ,EACM;EACvE,MAAM;EACN,GAAG;EACH,CAAC;CAGF,MAAM,aAAa,KAAK,OAAO,IAAI,SAAS,QAAQ,OAAO;CAC3D,MAAM,iBAAiB,IAAI,SAAS,OAAO,QAAQ,OAAO,WAAW;AACrE,MAAK,IAAI,KAAK,GAAG,OAAO,QAAQ,EAAE,GACjC,MAAK,IAAI,MAAM,GAAG,QAAQ,OAAO,EAAE,KAAK;EACvC,MAAM,OAAO,KAAK,QAAQ;EAG1B,MAAM,YACL,aACA,KAAK,MACJ,OACE,IACA,SAAS,KACR,SAAS,QAAQ,KAAK,MAAO,SAAS,SAAS,KAAM,OAAO,GAC3D,KAAK,MAAO,SAAS,QAAQ,MAAO,MAAM,GAE9C;AACF,iBAAe,UAAU,OAAO,GAAG,UAAU;;AAG/C,QAAO"}
|
|
1
|
+
{"version":3,"file":"compare_image.mjs","names":["pixelmatch"],"sources":["../../src/shared/compare_image.ts"],"sourcesContent":["import pixelmatch from 'pixelmatch'\nimport { type Options, ssim } from 'ssim.js'\nimport { convertThresholdUnit } from './convert_threshold_unit.ts'\nimport type { ComparisonMethod, ImageSnapshotCompareOptions, PixelDiffOptions } from './types.ts'\n\nexport function compareImage<M extends ComparisonMethod>(\n\timg1: Uint8ClampedArray | Buffer,\n\timg2: Uint8ClampedArray | Buffer,\n\toutput: Uint8ClampedArray | Buffer,\n\twidth: number,\n\theight: number,\n\toptions: ImageSnapshotCompareOptions<M> = {} as any,\n) {\n\tconst pixelDiff =\n\t\toptions.comparisonMethod === 'ssim'\n\t\t\t? compareWithSsim(img1, img2, output, width, height, options.diffOptions)\n\t\t\t: pixelmatch(img1, img2, output, width, height, options.diffOptions)\n\tconst diffAmount = convertThresholdUnit(\n\t\t{ failureThresholdType: options.failureThresholdType ?? 'pixel', width, height },\n\t\tpixelDiff,\n\t)\n\n\treturn {\n\t\tpass: diffAmount <= (options.failureThreshold ?? 0),\n\t\tdiffAmount,\n\t}\n}\n\n/**\n * A pixel counts as changed in the SSIM comparison when its SSIM value is below this.\n */\nexport const SSIM_CHANGED_THRESHOLD = 0.95\n\n/**\n * Returns one byte per pixel, non-zero where the pixel changed.\n *\n * The pixel method runs pixelmatch again in mask mode, so anti-aliased pixels are not included.\n * The SSIM method reads the diff written by {@link compareImage} and applies {@link SSIM_CHANGED_THRESHOLD}.\n */\nexport function getChangedMask<M extends ComparisonMethod>(\n\timg1: Uint8ClampedArray | Buffer,\n\timg2: Uint8ClampedArray | Buffer,\n\tdiff: Uint8ClampedArray | Buffer,\n\twidth: number,\n\theight: number,\n\toptions: ImageSnapshotCompareOptions<M> = {} as any,\n) {\n\tconst mask = new Uint8Array(width * height)\n\tif (options.comparisonMethod === 'ssim') {\n\t\t// `compareWithSsim()` writes `1 - ssim` to the alpha channel\n\t\tconst minAlpha = 0xff * (1 - SSIM_CHANGED_THRESHOLD)\n\t\tfor (let i = 0; i < mask.length; i++) if (diff[i * 4 + 3]! > minAlpha) mask[i] = 1\n\t\treturn mask\n\t}\n\tconst output = new Uint8ClampedArray(width * height * 4)\n\tpixelmatch(img1, img2, output, width, height, { ...(options.diffOptions as PixelDiffOptions), diffMask: true })\n\tfor (let i = 0; i < mask.length; i++) if (output[i * 4 + 3]) mask[i] = 1\n\treturn mask\n}\n\n/**\n * @author This code is based on `jest-image-snapshot` implementation.\n */\nfunction compareWithSsim(\n\timg1: Uint8ClampedArray | Buffer,\n\timg2: Uint8ClampedArray | Buffer,\n\toutput: Uint8ClampedArray | Buffer,\n\twidth: number,\n\theight: number,\n\tdiffOptions?: Partial<Options>,\n) {\n\tconst newImage = { data: img1, width: width, height: height }\n\tconst baselineImage = { data: img2, width: width, height: height }\n\tconst { ssim_map, mssim } = ssim(newImage as any, baselineImage as any, {\n\t\tssim: 'bezkrovny',\n\t\t...diffOptions,\n\t})\n\t// Converts the SSIM value to different pixels based on image width and height\n\t// conforms to how pixelmatch works.\n\tconst diffPixels = Math.round((1 - mssim) * width * height)\n\tconst diffRgbaPixels = new DataView(output.buffer, output.byteOffset)\n\tfor (let ln = 0; ln !== height; ++ln) {\n\t\tfor (let pos = 0; pos !== width; ++pos) {\n\t\t\tconst rPos = ln * width + pos\n\t\t\t// initial value is transparent. We'll add in the SSIM offset.\n\t\t\t// red (ff) green (00) blue (00) alpha (00)\n\t\t\tconst diffValue =\n\t\t\t\t0xff000000 +\n\t\t\t\tMath.floor(\n\t\t\t\t\t0xff *\n\t\t\t\t\t\t(1 -\n\t\t\t\t\t\t\tssim_map.data[\n\t\t\t\t\t\t\t\tssim_map.width * Math.round((ssim_map.height * ln) / height) +\n\t\t\t\t\t\t\t\t\tMath.round((ssim_map.width * pos) / width)\n\t\t\t\t\t\t\t]!),\n\t\t\t\t)\n\t\t\tdiffRgbaPixels.setUint32(rPos * 4, diffValue)\n\t\t}\n\t}\n\treturn diffPixels\n}\n"],"mappings":";;;;;AAKA,SAAgB,aACf,MACA,MACA,QACA,OACA,QACA,UAA0C,EAAE,EAC3C;CACD,MAAM,YACL,QAAQ,qBAAqB,SAC1B,gBAAgB,MAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,YAAY,GACvEA,WAAW,MAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,YAAY;CACtE,MAAM,aAAa,qBAClB;EAAE,sBAAsB,QAAQ,wBAAwB;EAAS;EAAO;EAAQ,EAChF,UACA;AAED,QAAO;EACN,MAAM,eAAe,QAAQ,oBAAoB;EACjD;EACA;;;;;AAMF,MAAa,yBAAyB;;;;;;;AAQtC,SAAgB,eACf,MACA,MACA,MACA,OACA,QACA,UAA0C,EAAE,EAC3C;CACD,MAAM,OAAO,IAAI,WAAW,QAAQ,OAAO;AAC3C,KAAI,QAAQ,qBAAqB,QAAQ;EAExC,MAAM,WAAW,OAAQ,IAAI;AAC7B,OAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,IAAK,KAAI,KAAK,IAAI,IAAI,KAAM,SAAU,MAAK,KAAK;AACjF,SAAO;;CAER,MAAM,SAAS,IAAI,kBAAkB,QAAQ,SAAS,EAAE;AACxD,YAAW,MAAM,MAAM,QAAQ,OAAO,QAAQ;EAAE,GAAI,QAAQ;EAAkC,UAAU;EAAM,CAAC;AAC/G,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,IAAK,KAAI,OAAO,IAAI,IAAI,GAAI,MAAK,KAAK;AACvE,QAAO;;;;;AAMR,SAAS,gBACR,MACA,MACA,QACA,OACA,QACA,aACC;CAGD,MAAM,EAAE,UAAU,UAAU,KAFX;EAAE,MAAM;EAAa;EAAe;EAAQ,EACvC;EAAE,MAAM;EAAa;EAAe;EAAQ,EACM;EACvE,MAAM;EACN,GAAG;EACH,CAAC;CAGF,MAAM,aAAa,KAAK,OAAO,IAAI,SAAS,QAAQ,OAAO;CAC3D,MAAM,iBAAiB,IAAI,SAAS,OAAO,QAAQ,OAAO,WAAW;AACrE,MAAK,IAAI,KAAK,GAAG,OAAO,QAAQ,EAAE,GACjC,MAAK,IAAI,MAAM,GAAG,QAAQ,OAAO,EAAE,KAAK;EACvC,MAAM,OAAO,KAAK,QAAQ;EAG1B,MAAM,YACL,aACA,KAAK,MACJ,OACE,IACA,SAAS,KACR,SAAS,QAAQ,KAAK,MAAO,SAAS,SAAS,KAAM,OAAO,GAC3D,KAAK,MAAO,SAAS,QAAQ,MAAO,MAAM,GAE9C;AACF,iBAAe,UAAU,OAAO,GAAG,UAAU;;AAG/C,QAAO"}
|
|
@@ -4,6 +4,8 @@ declare const SNAPSHOT_ROOT_DIR = "__vis__";
|
|
|
4
4
|
declare const BASELINE_DIR = "__baselines__";
|
|
5
5
|
declare const RESULT_DIR = "__results__";
|
|
6
6
|
declare const DIFF_DIR = "__diffs__";
|
|
7
|
+
declare const REVIEW_IMAGE_SUFFIX = ".review.png";
|
|
8
|
+
declare const CHANGED_REGIONS_SUFFIX = ".regions.json";
|
|
7
9
|
//#endregion
|
|
8
|
-
export { BASELINE_DIR, DIFF_DIR, NAME, RESULT_DIR, SNAPSHOT_ROOT_DIR };
|
|
10
|
+
export { BASELINE_DIR, CHANGED_REGIONS_SUFFIX, DIFF_DIR, NAME, RESULT_DIR, REVIEW_IMAGE_SUFFIX, SNAPSHOT_ROOT_DIR };
|
|
9
11
|
//# sourceMappingURL=constants.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.mts","names":[],"sources":["../../src/shared/constants.ts"],"sourcesContent":[],"mappings":";cAAa,IAAA;AAAA,cACA,iBAAA,GADI,SAAA;AACJ,cACA,YAAA,GADiB,eAAA;AACjB,cACA,UAAA,GADY,aAAA;AACZ,cACA,QAAA,GADU,WAAA"}
|
|
1
|
+
{"version":3,"file":"constants.d.mts","names":[],"sources":["../../src/shared/constants.ts"],"sourcesContent":[],"mappings":";cAAa,IAAA;AAAA,cACA,iBAAA,GADI,SAAA;AACJ,cACA,YAAA,GADiB,eAAA;AACjB,cACA,UAAA,GADY,aAAA;AACZ,cACA,QAAA,GADU,WAAA;AACV,cACA,mBAAA,GADQ,aAAA;AACR,cACA,sBAAA,GADmB,eAAA"}
|