|
| 1 | +import assert from "node:assert/strict" |
| 2 | +import { readFile, writeFile } from "node:fs/promises" |
| 3 | +import { join } from "node:path" |
| 4 | + |
| 5 | +import { PNG } from "pngjs" |
| 6 | +import { commandRegistry } from "../packages/runtime-core/src/command-registry.js" |
| 7 | +import { runVisualCompareCommand } from "../packages/runtime-playground/dist/browser-visual-compare.js" |
| 8 | +import { withTempDir } from "../scripts/test-kit.js" |
| 9 | + |
| 10 | +const visualCompare = commandRegistry.find((definition) => definition.id === "wordpress.visual-compare") |
| 11 | +assert.ok(visualCompare, "wordpress.visual-compare is registered") |
| 12 | + |
| 13 | +const acceptedArgs = visualCompare.acceptedArgs |
| 14 | +const maxElements = acceptedArgs.find((arg) => arg.name === "max-explanation-elements") |
| 15 | +const maxCandidates = acceptedArgs.find((arg) => arg.name === "max-explanation-candidates") |
| 16 | +const selector = acceptedArgs.find((arg) => arg.name === "explain-selector") |
| 17 | +assert.deepEqual(maxElements && { format: maxElements.format }, { format: "positive integer" }) |
| 18 | +assert.deepEqual(maxCandidates && { format: maxCandidates.format }, { format: "positive integer" }) |
| 19 | +assert.deepEqual(selector && { repeatable: selector.repeatable, format: selector.format }, { repeatable: true, format: "CSS selector" }) |
| 20 | +const matrixDescription = acceptedArgs.find((arg) => arg.name === "matrix-json")?.description ?? "" |
| 21 | +for (const field of ["maxExplanationElements", "maxExplanationCandidates", "explainSelectors", "max-explanation-elements", "max-explanation-candidates", "explain-selector"]) { |
| 22 | + assert.match(matrixDescription, new RegExp(field)) |
| 23 | +} |
| 24 | + |
| 25 | +async function writePng(path: string): Promise<void> { |
| 26 | + const png = new PNG({ width: 1, height: 1 }) |
| 27 | + png.data.set([255, 255, 255, 255]) |
| 28 | + await writeFile(path, PNG.sync.write(png)) |
| 29 | +} |
| 30 | + |
| 31 | +async function visualCompareRun(artifactRoot: string, args: string[]) { |
| 32 | + return runVisualCompareCommand({ |
| 33 | + artifactRoot, |
| 34 | + server: { |
| 35 | + serverUrl: "http://127.0.0.1:1", |
| 36 | + playground: { run: async () => ({ text: "" }) }, |
| 37 | + async [Symbol.asyncDispose]() {}, |
| 38 | + }, |
| 39 | + spec: { command: "wordpress.visual-compare", args }, |
| 40 | + }) |
| 41 | +} |
| 42 | + |
| 43 | +const expectedOptions = (maxExplanationElements: number, maxExplanationCandidates: number, explainSelectors?: string[]) => ({ |
| 44 | + waitFor: "domcontentloaded", |
| 45 | + durationMs: 0, |
| 46 | + timeoutMs: 120_000, |
| 47 | + fullPage: true, |
| 48 | + maxFullPageHeight: 20_000, |
| 49 | + threshold: 0.1, |
| 50 | + includeAA: false, |
| 51 | + maxRegions: 8, |
| 52 | + maxExplanationElements, |
| 53 | + maxExplanationCandidates, |
| 54 | + ...(explainSelectors ? { explainSelectors } : {}), |
| 55 | +}) |
| 56 | + |
| 57 | +await withTempDir("wp-codebox-visual-compare-contract-", async (artifactRoot) => { |
| 58 | + const sourceScreenshot = join(artifactRoot, "source.png") |
| 59 | + const candidateScreenshot = join(artifactRoot, "candidate.png") |
| 60 | + await Promise.all([writePng(sourceScreenshot), writePng(candidateScreenshot)]) |
| 61 | + |
| 62 | + const defaults = JSON.parse((await visualCompareRun(artifactRoot, [`source-screenshot=${sourceScreenshot}`, `candidate-screenshot=${candidateScreenshot}`])).output) |
| 63 | + assert.deepEqual(defaults.options, expectedOptions(25, 160)) |
| 64 | + |
| 65 | + const pair = JSON.parse((await visualCompareRun(artifactRoot, [ |
| 66 | + `source-screenshot=${sourceScreenshot}`, |
| 67 | + `candidate-screenshot=${candidateScreenshot}`, |
| 68 | + "max-explanation-elements=40", |
| 69 | + "max-explanation-candidates=240", |
| 70 | + "explain-selector=main", |
| 71 | + "explain-selector=body", |
| 72 | + ])).output) |
| 73 | + assert.deepEqual(pair.options, expectedOptions(40, 240, ["main", "body"])) |
| 74 | + assert.equal(pair.schema, "wp-codebox/visual-compare/v1") |
| 75 | + assert.equal(pair.command, "wordpress.visual-compare") |
| 76 | + assert.equal(pair.status, "identical") |
| 77 | + assert.deepEqual(pair.files, { |
| 78 | + sourceScreenshot: "files/browser/visual-compare/source.png", |
| 79 | + candidateScreenshot: "files/browser/visual-compare/candidate.png", |
| 80 | + diffScreenshot: "files/browser/visual-compare/diff.png", |
| 81 | + visualDiff: "files/browser/visual-compare/visual-diff.json", |
| 82 | + blocksEngineVisualParity: "files/browser/visual-compare/blocks-engine-visual-parity-report.json", |
| 83 | + summary: "files/browser/visual-compare/summary.json", |
| 84 | + }) |
| 85 | + assert.deepEqual(pair.comparison, { |
| 86 | + source: { width: 1, height: 1 }, |
| 87 | + candidate: { width: 1, height: 1 }, |
| 88 | + diff: { width: 1, height: 1 }, |
| 89 | + mismatchPixels: 0, |
| 90 | + totalPixels: 1, |
| 91 | + mismatchRatio: 0, |
| 92 | + overlapMismatchPixels: 0, |
| 93 | + overlapPixels: 1, |
| 94 | + overlapMismatchRatio: 0, |
| 95 | + dimensionMismatch: false, |
| 96 | + dimensionDeltaPixels: 0, |
| 97 | + dimensionDeltaRatio: 0, |
| 98 | + regions: [], |
| 99 | + }) |
| 100 | + for (const hash of Object.values(pair.hashes) as Array<{ algorithm: string; value: string }>) { |
| 101 | + assert.equal(hash.algorithm, "sha256") |
| 102 | + assert.match(hash.value, /^[a-f0-9]{64}$/) |
| 103 | + } |
| 104 | + const persistedPair = JSON.parse(await readFile(join(artifactRoot, pair.files.summary), "utf8")) |
| 105 | + assert.deepEqual(persistedPair.options, pair.options) |
| 106 | + assert.deepEqual(persistedPair.files, pair.files) |
| 107 | + |
| 108 | + for (const [arg, message] of [["max-explanation-elements=0", "max-explanation-elements"], ["max-explanation-candidates=0", "max-explanation-candidates"], ["max-explanation-elements=1.5", "max-explanation-elements"], ["max-explanation-candidates=160px", "max-explanation-candidates"]]) { |
| 109 | + await assert.rejects(visualCompareRun(artifactRoot, [`source-screenshot=${sourceScreenshot}`, `candidate-screenshot=${candidateScreenshot}`, arg]), new RegExp(`${message} must be a positive integer`)) |
| 110 | + } |
| 111 | + |
| 112 | + const matrix = JSON.parse((await visualCompareRun(artifactRoot, [ |
| 113 | + "explain-selector=main", |
| 114 | + "explain-selector=body", |
| 115 | + `matrix-json=${JSON.stringify({ comparisons: [{ name: "camel-case", sourceScreenshot, candidateScreenshot, maxExplanationElements: 50, maxExplanationCandidates: 300, explainSelectors: ["body", "article"] }, { name: "kebab-case", "source-screenshot": sourceScreenshot, "candidate-screenshot": candidateScreenshot, "max-explanation-elements": 60, "max-explanation-candidates": 320, "explain-selector": "article" }] })}`, |
| 116 | + ])).output) |
| 117 | + assert.equal(matrix.schema, "wp-codebox/visual-compare-matrix/v1") |
| 118 | + assert.equal(matrix.command, "wordpress.visual-compare") |
| 119 | + assert.equal(matrix.complete, true) |
| 120 | + assert.deepEqual(matrix.metrics, { expectedComparisons: 2, comparisons: 2, missing: 0, failed: 0, identical: 2, different: 0, maxMismatchRatio: 0, meanMismatchRatio: 0, maxOverlapMismatchRatio: 0, meanOverlapMismatchRatio: 0, maxMismatchPixels: 0, meanMismatchPixels: 0 }) |
| 121 | + assert.deepEqual(matrix.comparisons[0].options, expectedOptions(50, 300, ["main", "body", "article"])) |
| 122 | + assert.deepEqual(matrix.comparisons[1].options, expectedOptions(60, 320, ["main", "body", "article"])) |
| 123 | + const persistedMatrix = JSON.parse(await readFile(join(artifactRoot, matrix.files.summary), "utf8")) |
| 124 | + assert.deepEqual(persistedMatrix.comparisons.map((comparison: { options: unknown }) => comparison.options), matrix.comparisons.map((comparison: { options: unknown }) => comparison.options)) |
| 125 | + |
| 126 | + for (const [field, value] of [["maxExplanationElements", "1.5"], ["maxExplanationCandidates", 0]]) { |
| 127 | + await assert.rejects( |
| 128 | + visualCompareRun(artifactRoot, [`matrix-json=${JSON.stringify({ comparisons: [{ sourceScreenshot, candidateScreenshot, [field]: value }] })}`]), |
| 129 | + new RegExp(`${field === "maxExplanationElements" ? "max-explanation-elements" : "max-explanation-candidates"} must be a positive integer`), |
| 130 | + ) |
| 131 | + } |
| 132 | +}) |
| 133 | + |
| 134 | +console.log("browser visual compare contract passed") |
0 commit comments