mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 01:56:37 +03:00
Add comp-diff, comp-spec, and build-phase: measured comp fidelity for the build phase
Dependency-free PNG codec, perceptual metrics (structure / color / detail / bands), side-by-side + heatmap + per-region crops, a measured spec from the approved comp (grid overlay, sampled palette, plate list), and a phase state machine whose spec / plates / hero gates run the diff instead of asking the model to remember the image. AI-assisted (Claude). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
Abdul Wahab
co-authored by
Claude
parent
f86473ba7d
commit
b0fc2e8801
@@ -0,0 +1,168 @@
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { encodePng } from '../skill/scripts/lib/png.mjs';
|
||||
import { createImage, fillRect, blit, resize } from '../skill/scripts/lib/raster.mjs';
|
||||
import { gridToBox, measureRegions, platePrompt } from '../skill/scripts/comp-spec.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const SPEC_SCRIPT = path.join(ROOT, 'skill', 'scripts', 'comp-spec.mjs');
|
||||
const PHASE_SCRIPT = path.join(ROOT, 'skill', 'scripts', 'build-phase.mjs');
|
||||
|
||||
function lcg(seed) { let s = seed >>> 0; return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff); }
|
||||
|
||||
function makeComp(w = 640, h = 400) {
|
||||
const img = createImage(w, h, [240, 237, 226, 255]);
|
||||
fillRect(img, 0, 0, w, 40, [19, 33, 48, 255]);
|
||||
fillRect(img, 20, 60, 220, 20, [19, 33, 48, 255]);
|
||||
fillRect(img, 20, 90, 180, 20, [19, 33, 48, 255]);
|
||||
const rnd = lcg(3);
|
||||
for (let y = 40; y < 200; y++) for (let x = 320; x < 640; x++) {
|
||||
const v = 100 + Math.floor(rnd() * 140);
|
||||
const p = (y * w + x) * 4; img.data[p] = v; img.data[p + 1] = v; img.data[p + 2] = v;
|
||||
}
|
||||
for (let i = 0; i < 3; i++) { fillRect(img, 0, 220 + i * 50, w, 1, [19, 33, 48, 255]); fillRect(img, 20, 232 + i * 50, 300, 12, [19, 33, 48, 255]); }
|
||||
return img;
|
||||
}
|
||||
|
||||
function run(script, args, cwd) {
|
||||
return spawnSync(process.execPath, [script, ...args], { cwd, encoding: 'utf8' });
|
||||
}
|
||||
|
||||
describe('comp-spec', () => {
|
||||
it('parses grid spans into normalized boxes', () => {
|
||||
assert.deepEqual(gridToBox('A0:A0'), { x: 0, y: 0, w: 0.1, h: 0.1 });
|
||||
assert.deepEqual(gridToBox('E0:J4'), { x: 0.4, y: 0, w: 0.6, h: 0.5 });
|
||||
assert.deepEqual(gridToBox('j4:e0'), { x: 0.4, y: 0, w: 0.6, h: 0.5 });
|
||||
assert.throws(() => gridToBox('K0:A1'));
|
||||
});
|
||||
|
||||
it('measures regions with palette, pixel box, medium, and plate path', () => {
|
||||
const comp = makeComp();
|
||||
const spec = measureRegions(comp, { regions: [
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' },
|
||||
{ id: 'art', kind: 'plate', grid: 'F1:J4', note: 'noise plate' },
|
||||
] }, 'comp.png');
|
||||
assert.equal(spec.regions.length, 2);
|
||||
const art = spec.regions.find((r) => r.id === 'art');
|
||||
assert.equal(art.medium, 'raster');
|
||||
assert.equal(art.plate, path.join('assets', 'plates', 'art.png'));
|
||||
assert.equal(art.px.x, 320);
|
||||
assert.ok(art.palette.length > 0);
|
||||
assert.equal(spec.regions[0].medium, 'semantic');
|
||||
assert.equal(spec.orientation, 'landscape');
|
||||
assert.match(platePrompt(spec, art), /noise plate/);
|
||||
});
|
||||
|
||||
it('rejects duplicate ids and missing ids', () => {
|
||||
const comp = makeComp();
|
||||
assert.throws(() => measureRegions(comp, { regions: [{ id: 'a', grid: 'A0:A0' }, { id: 'a', grid: 'B0:B0' }] }, 'c.png'), /duplicate/);
|
||||
assert.throws(() => measureRegions(comp, { regions: [{ grid: 'A0:A0' }] }, 'c.png'), /id/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('build-phase state machine (CLI)', () => {
|
||||
let dir;
|
||||
before(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'build-phase-'));
|
||||
fs.writeFileSync(path.join(dir, 'comp.png'), encodePng(makeComp()));
|
||||
fs.writeFileSync(path.join(dir, 'regions.json'), JSON.stringify({ regions: [
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' },
|
||||
{ id: 'headline', kind: 'text', grid: 'A1:D2' },
|
||||
{ id: 'art', kind: 'plate', grid: 'F1:J4', note: 'noise plate' },
|
||||
{ id: 'list', kind: 'control', grid: 'A5:J9' },
|
||||
] }));
|
||||
});
|
||||
after(() => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} });
|
||||
|
||||
it('start writes state at spec and prints NEXT', () => {
|
||||
const res = run(PHASE_SCRIPT, ['start', '--comp', 'comp.png'], dir);
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.match(res.stdout, /BUILD-PHASE SPEC/);
|
||||
assert.match(res.stdout, /NEXT Measure the comp/);
|
||||
assert.ok(fs.existsSync(path.join(dir, '.impeccable', 'build', 'state.json')));
|
||||
});
|
||||
|
||||
it('spec gate fails without a spec, passes once comp-spec wrote one', () => {
|
||||
let res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
assert.equal(res.status, 2);
|
||||
assert.match(res.stdout, /GATE SPEC FAILED/);
|
||||
res = run(SPEC_SCRIPT, ['--comp', 'comp.png', '--grid'], dir);
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.ok(fs.existsSync(path.join(dir, '.impeccable', 'build', 'comp-grid.png')));
|
||||
res = run(SPEC_SCRIPT, ['--comp', 'comp.png', '--regions', 'regions.json'], dir);
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.match(res.stdout, /PLATES 1 to produce: art/);
|
||||
res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
assert.equal(res.status, 0, res.stdout + res.stderr);
|
||||
assert.match(res.stdout, /ADVANCED spec -> plates/);
|
||||
});
|
||||
|
||||
it('plates gate names the missing plate, rejects a comp-size crop, accepts a 2x plate', () => {
|
||||
let res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
assert.equal(res.status, 2);
|
||||
assert.match(res.stdout, /plate missing for art/);
|
||||
// comp-size crop: too small
|
||||
res = run(SPEC_SCRIPT, ['--crop', 'art', '--out', 'assets/plates/art.png'], dir);
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
assert.equal(res.status, 2);
|
||||
assert.match(res.stdout, /needs at least 1.5x/);
|
||||
// 2x crop passes size and similarity
|
||||
res = run(SPEC_SCRIPT, ['--crop', 'art', '--scale', '2', '--out', 'assets/plates/art.png'], dir);
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
assert.equal(res.status, 0, res.stdout);
|
||||
assert.match(res.stdout, /ADVANCED plates -> hero/);
|
||||
});
|
||||
|
||||
it('hero gate fails on a flat build and passes on a faithful one, recording attempts', () => {
|
||||
const comp = makeComp();
|
||||
const flat = createImage(comp.width, comp.height, [240, 237, 226, 255]);
|
||||
fillRect(flat, 0, 0, comp.width, 40, [19, 33, 48, 255]);
|
||||
fs.mkdirSync(path.join(dir, '.impeccable', 'review'), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, '.impeccable', 'review', 'hero-repro.png'), encodePng(flat));
|
||||
let res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
assert.equal(res.status, 2, res.stdout);
|
||||
assert.match(res.stdout, /GATE HERO FAILED/);
|
||||
assert.match(res.stdout, /region art is missing/);
|
||||
assert.ok(fs.existsSync(path.join(dir, '.impeccable', 'review', 'diff', 'hero', 'side-by-side.png')));
|
||||
// faithful: the comp shifted by a few px, captured at 1.5x width
|
||||
const shifted = createImage(comp.width, comp.height, [240, 237, 226, 255]);
|
||||
blit(shifted, comp, 3, 2);
|
||||
fs.writeFileSync(path.join(dir, '.impeccable', 'review', 'hero-repro.png'), encodePng(resize(shifted, comp.width * 1.5, comp.height * 1.5)));
|
||||
res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
assert.equal(res.status, 0, res.stdout);
|
||||
assert.match(res.stdout, /ADVANCED hero -> sections/);
|
||||
const state = JSON.parse(fs.readFileSync(path.join(dir, '.impeccable', 'build', 'state.json'), 'utf8'));
|
||||
assert.equal(state.phases.hero.attempts, 2);
|
||||
assert.ok(state.phases.hero.gate.score >= 0.72);
|
||||
});
|
||||
|
||||
it('later phases advance without a gate; force is recorded; finish records the disposition', () => {
|
||||
for (const from of ['sections', 'motion']) {
|
||||
const res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
assert.equal(res.status, 0, res.stdout);
|
||||
assert.match(res.stdout, new RegExp(`ADVANCED ${from}`));
|
||||
}
|
||||
let res = run(PHASE_SCRIPT, ['advance', '--force', '--reason', 'test'], dir);
|
||||
assert.equal(res.status, 0);
|
||||
res = run(PHASE_SCRIPT, ['finish', '--disposition', 'fix'], dir);
|
||||
assert.equal(res.status, 0);
|
||||
assert.match(res.stdout, /finish fix/);
|
||||
res = run(PHASE_SCRIPT, ['status', '--json'], dir);
|
||||
const state = JSON.parse(res.stdout);
|
||||
assert.equal(state.phase, 'review');
|
||||
assert.equal(state.finish.disposition, 'fix');
|
||||
});
|
||||
|
||||
it('refuses a bad disposition and an unknown command', () => {
|
||||
assert.equal(run(PHASE_SCRIPT, ['finish', '--disposition', 'great'], dir).status, 1);
|
||||
assert.equal(run(PHASE_SCRIPT, ['dance'], dir).status, 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
import { describe, it, before } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { decodePng, encodePng } from '../skill/scripts/lib/png.mjs';
|
||||
import { createImage, fillRect, blit, crop, resize, drawText } from '../skill/scripts/lib/raster.mjs';
|
||||
import { compare, verdictFor, alignBuild } from '../skill/scripts/comp-diff.mjs';
|
||||
import { dominantColors, structureScore, detailScore } from '../skill/scripts/lib/image-metrics.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const SCRIPT = path.join(ROOT, 'skill', 'scripts', 'comp-diff.mjs');
|
||||
|
||||
// A synthetic "comp": bone ground, navy masthead, a red-gutter table, and a
|
||||
// noisy "illustration" region on the right of the fold. Deterministic noise.
|
||||
function lcg(seed) { let s = seed >>> 0; return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff); }
|
||||
|
||||
function makeComp(w = 768, h = 512) {
|
||||
const img = createImage(w, h, [240, 237, 226, 255]);
|
||||
fillRect(img, 0, 0, w, 32, [19, 33, 48, 255]); // masthead
|
||||
drawText(img, 'CARBURETOR CLUB', 12, 8, [240, 237, 226, 255], 2);
|
||||
// headline block
|
||||
fillRect(img, 24, 70, 280, 22, [19, 33, 48, 255]);
|
||||
fillRect(img, 24, 100, 240, 22, [19, 33, 48, 255]);
|
||||
fillRect(img, 24, 136, 90, 4, [176, 40, 32, 255]);
|
||||
// illustration: high-frequency noise plate
|
||||
const rnd = lcg(7);
|
||||
for (let y = 48; y < 240; y++) for (let x = 340; x < 740; x++) {
|
||||
const v = 120 + Math.floor(rnd() * 120);
|
||||
const p = (y * w + x) * 4; img.data[p] = v; img.data[p + 1] = v; img.data[p + 2] = v + 10;
|
||||
}
|
||||
// table with red gutter
|
||||
fillRect(img, 0, 260, w, 2, [19, 33, 48, 255]);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const y = 270 + i * 60;
|
||||
fillRect(img, 0, y, 10, 50, i === 0 ? [176, 40, 32, 255] : [19, 33, 48, 255]);
|
||||
fillRect(img, 30, y + 10, 300, 14, [19, 33, 48, 255]);
|
||||
fillRect(img, 30, y + 30, 200, 8, [120, 120, 120, 255]);
|
||||
fillRect(img, 0, y + 56, w, 1, [19, 33, 48, 255]);
|
||||
}
|
||||
// CTA
|
||||
fillRect(img, 24, 460, 160, 36, [19, 33, 48, 255]);
|
||||
return img;
|
||||
}
|
||||
|
||||
function flattenIllustration(comp) {
|
||||
const img = { ...comp, data: new Uint8Array(comp.data) };
|
||||
fillRect(img, 340, 48, 400, 192, [200, 200, 205, 255]); // a flat gray box where the plate was
|
||||
return img;
|
||||
}
|
||||
|
||||
function recolor(comp) {
|
||||
const img = { ...comp, data: new Uint8Array(comp.data) };
|
||||
for (let i = 0; i < img.data.length; i += 4) {
|
||||
if (img.data[i] > 220 && img.data[i + 1] > 210) { img.data[i] = 20; img.data[i + 1] = 40; img.data[i + 2] = 60; }
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
function shifted(comp, dx, dy) {
|
||||
const img = createImage(comp.width, comp.height, [240, 237, 226, 255]);
|
||||
blit(img, comp, dx, dy);
|
||||
return img;
|
||||
}
|
||||
|
||||
function tallPage(comp) {
|
||||
// a full-page screenshot: comp on top, then more page below
|
||||
const img = createImage(comp.width, comp.height * 3, [240, 237, 226, 255]);
|
||||
blit(img, comp, 0, 0);
|
||||
fillRect(img, 0, comp.height + 40, comp.width, 200, [19, 33, 48, 255]);
|
||||
return img;
|
||||
}
|
||||
|
||||
const SPEC = {
|
||||
regions: [
|
||||
{ id: 'masthead', kind: 'control', box: { x: 0, y: 0, w: 1, h: 32 / 512 } },
|
||||
{ id: 'headline', kind: 'text', box: { x: 0.02, y: 60 / 512, w: 0.4, h: 100 / 512 } },
|
||||
{ id: 'plate', kind: 'plate', box: { x: 340 / 768, y: 48 / 512, w: 400 / 768, h: 192 / 512 } },
|
||||
{ id: 'table', kind: 'control', box: { x: 0, y: 260 / 512, w: 1, h: 190 / 512 } },
|
||||
],
|
||||
};
|
||||
|
||||
describe('png codec', () => {
|
||||
it('round-trips RGBA through encode/decode', () => {
|
||||
const img = createImage(20, 10, [10, 20, 30, 255]);
|
||||
img.data.set([200, 100, 50, 128], (2 * 20 + 2) * 4);
|
||||
const back = decodePng(encodePng(img, { text: { 'impeccable:prompt': 'hello' } }));
|
||||
assert.equal(back.width, 20); assert.equal(back.height, 10);
|
||||
assert.deepEqual([...back.data.subarray(0, 4)], [10, 20, 30, 255]);
|
||||
assert.deepEqual([...back.data.subarray((2 * 20 + 2) * 4, (2 * 20 + 2) * 4 + 4)], [200, 100, 50, 128]);
|
||||
assert.equal(back.text['impeccable:prompt'], 'hello');
|
||||
});
|
||||
|
||||
it('decodes a real gpt-image / Playwright style PNG when one is on disk (skips otherwise)', () => {
|
||||
const sample = path.join(ROOT, 'tests', 'fixtures', 'comp-fidelity', 'sample.png');
|
||||
if (!fs.existsSync(sample)) return;
|
||||
const img = decodePng(fs.readFileSync(sample));
|
||||
assert.ok(img.width > 0 && img.height > 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('image metrics', () => {
|
||||
const comp = makeComp();
|
||||
it('identity scores 1', () => {
|
||||
assert.ok(structureScore(comp, comp) > 0.999);
|
||||
assert.ok(detailScore(comp, comp).score > 0.999);
|
||||
});
|
||||
it('dominant colors find the ground and the ink', () => {
|
||||
const cols = dominantColors(comp).map((c) => c.hex);
|
||||
assert.ok(cols.some((h) => h.startsWith('#f') || h.startsWith('#e')), `ground missing in ${cols}`);
|
||||
assert.ok(cols.some((h) => h.startsWith('#1') || h.startsWith('#0') || h.startsWith('#2')), `ink missing in ${cols}`);
|
||||
});
|
||||
it('a flattened plate loses detail', () => {
|
||||
const d = detailScore(comp, flattenIllustration(comp));
|
||||
assert.ok(d.score < 0.85, `expected detail loss, got ${d.score}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('comp-diff compare', () => {
|
||||
const comp = makeComp();
|
||||
|
||||
it('scores the comp against itself as a match everywhere', () => {
|
||||
const r = compare({ comp, build: comp, spec: SPEC });
|
||||
assert.equal(r.whole.overall, 1);
|
||||
for (const region of r.regions) assert.equal(region.verdict, 'match', region.id);
|
||||
});
|
||||
|
||||
it('forgives a small translation', () => {
|
||||
const r = compare({ comp, build: shifted(comp, 6, 4), spec: SPEC });
|
||||
assert.ok(r.whole.overall >= 0.8, `overall ${r.whole.overall}`);
|
||||
assert.equal(verdictFor(r.whole), 'match');
|
||||
});
|
||||
|
||||
it('reads the top of a full-page screenshot as the first viewport', () => {
|
||||
const r = compare({ comp, build: tallPage(comp), spec: SPEC });
|
||||
assert.equal(r.aligned.height, comp.height);
|
||||
assert.ok(r.whole.overall > 0.95, `overall ${r.whole.overall}`);
|
||||
});
|
||||
|
||||
it('flags a flattened plate region as missing while the rest matches', () => {
|
||||
const r = compare({ comp, build: flattenIllustration(comp), spec: SPEC });
|
||||
const plate = r.regions.find((x) => x.id === 'plate');
|
||||
assert.equal(plate.verdict, 'missing');
|
||||
assert.equal(r.regions.find((x) => x.id === 'table').verdict, 'match');
|
||||
assert.equal(r.regions.find((x) => x.id === 'masthead').verdict, 'match');
|
||||
});
|
||||
|
||||
it('fails a recolored page on color and structure', () => {
|
||||
const r = compare({ comp, build: recolor(comp), spec: SPEC });
|
||||
assert.ok(r.whole.color < 0.7, `color ${r.whole.color}`);
|
||||
assert.ok(r.whole.overall < 0.6, `overall ${r.whole.overall}`);
|
||||
assert.notEqual(verdictFor(r.whole), 'match');
|
||||
});
|
||||
|
||||
it('derives band regions when no spec is given', () => {
|
||||
const r = compare({ comp, build: comp });
|
||||
assert.ok(r.regions.length >= 2);
|
||||
assert.ok(r.regions.every((x) => x.kind === 'band'));
|
||||
});
|
||||
|
||||
it('pads a shorter build with white so a truncated page reads as missing content', () => {
|
||||
const half = crop(comp, 0, 0, comp.width, comp.height / 2);
|
||||
const aligned = alignBuild(comp, half);
|
||||
assert.equal(aligned.height, comp.height);
|
||||
const r = compare({ comp, build: half, spec: SPEC });
|
||||
assert.notEqual(r.regions.find((x) => x.id === 'table').verdict, 'match');
|
||||
});
|
||||
|
||||
it('scales a build captured at a different width onto the comp', () => {
|
||||
const wide = resize(comp, comp.width * 1.5, comp.height * 1.5);
|
||||
const r = compare({ comp, build: wide, spec: SPEC });
|
||||
assert.ok(r.whole.overall > 0.9, `overall ${r.whole.overall}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('comp-diff CLI', () => {
|
||||
let dir;
|
||||
before(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'comp-diff-'));
|
||||
const comp = makeComp();
|
||||
fs.writeFileSync(path.join(dir, 'comp.png'), encodePng(comp));
|
||||
fs.writeFileSync(path.join(dir, 'flat.png'), encodePng(flattenIllustration(comp)));
|
||||
fs.writeFileSync(path.join(dir, 'spec.json'), JSON.stringify(SPEC));
|
||||
});
|
||||
|
||||
it('writes side-by-side, heatmap, region pairs, and report.json', () => {
|
||||
const out = path.join(dir, 'diff');
|
||||
const res = spawnSync(process.execPath, [SCRIPT, '--comp', path.join(dir, 'comp.png'), '--build', path.join(dir, 'flat.png'), '--spec', path.join(dir, 'spec.json'), '--out-dir', out], { encoding: 'utf8' });
|
||||
assert.equal(res.status, 0, res.stderr + res.stdout);
|
||||
assert.match(res.stdout, /^COMP-DIFF/m);
|
||||
assert.match(res.stdout, /REGION plate\s+missing/);
|
||||
for (const f of ['side-by-side.png', 'heatmap.png', 'report.json', path.join('regions', 'plate.png')]) {
|
||||
assert.ok(fs.existsSync(path.join(out, f)), `${f} missing`);
|
||||
}
|
||||
const report = JSON.parse(fs.readFileSync(path.join(out, 'report.json'), 'utf8'));
|
||||
assert.equal(report.tool, 'comp-diff');
|
||||
assert.equal(report.regions.length, 4);
|
||||
assert.ok(report.palette.comp.length > 0);
|
||||
// artifacts decode
|
||||
const side = decodePng(fs.readFileSync(path.join(out, 'side-by-side.png')));
|
||||
assert.ok(side.width > 768);
|
||||
});
|
||||
|
||||
it('exits 3 below --threshold and prints the instruction', () => {
|
||||
const res = spawnSync(process.execPath, [SCRIPT, '--comp', path.join(dir, 'comp.png'), '--build', path.join(dir, 'flat.png'), '--no-files', '--threshold', '0.99'], { encoding: 'utf8' });
|
||||
assert.equal(res.status, 3);
|
||||
assert.match(res.stdout, /BELOW THRESHOLD/);
|
||||
});
|
||||
|
||||
it('--json prints the report', () => {
|
||||
const res = spawnSync(process.execPath, [SCRIPT, '--comp', path.join(dir, 'comp.png'), '--build', path.join(dir, 'comp.png'), '--no-files', '--json'], { encoding: 'utf8' });
|
||||
assert.equal(res.status, 0);
|
||||
const report = JSON.parse(res.stdout);
|
||||
assert.equal(report.verdict, 'match');
|
||||
});
|
||||
|
||||
it('exits 1 with usage on missing args', () => {
|
||||
const res = spawnSync(process.execPath, [SCRIPT], { encoding: 'utf8' });
|
||||
assert.equal(res.status, 1);
|
||||
assert.match(res.stderr, /usage/);
|
||||
});
|
||||
});
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
Reference in New Issue
Block a user