Check the mouth interior by its own tone, and stop teardown failing the run
Two bugs in the browser suite, both dating from step 7 adding eyes, brows and teeth to the stage. The shut-mouth check asked for exactly one tone on the whole canvas. That was true when the stage held nothing but a mouth; since step 7 every frame carries five or six tones whatever the mouth is doing, so the check failed on a correct picture. Ask instead whether :mouth-dark is on the stage at all: nothing else carries that tone, so it answers "is the interior drawn" without needing to know where the mouth is. freeze_test already got this same rescoping; its browser twin did not. The failure went unnoticed because close() raced Chrome's own profile writes and threw ENOTEMPTY out of main's `finally`, past the summary line and the process.exit that reports the failure count. The suite therefore exited 2 and printed no verdict whether it passed or failed. Teardown is now allowed to fail out loud without taking the exit code with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B87NVmiU36qQmN9gmFYnJ9
This commit is contained in:
parent
663b7c367a
commit
ccca93e233
1 changed files with 34 additions and 8 deletions
|
|
@ -37,6 +37,14 @@ const CHROME = process.env.CHROME ??
|
|||
// nothing drawn, and every check below is a count of pixels that are not it.
|
||||
const BG = [0x12, 0x14, 0x1c];
|
||||
|
||||
// :mouth-dark, from domain/palette, packed the way PROBE packs a tone. It is the
|
||||
// mouth INTERIOR's colour and nothing else on the stage carries it, so its
|
||||
// presence is the whole of "the mouth is open" without needing to know where the
|
||||
// mouth is. A whole-canvas tone COUNT cannot answer that any more: step 7 put
|
||||
// eyes, brows, an iris, a pupil and teeth on the same stage, so every frame has
|
||||
// several tones whatever the mouth is doing.
|
||||
const MOUTH_DARK = (0x24 << 16) | (0x16 << 8) | 0x1a;
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
let failures = 0;
|
||||
|
|
@ -122,7 +130,22 @@ async function connect() {
|
|||
const r = await this.send('Page.captureScreenshot', { format: 'png' });
|
||||
writeFileSync(join(OUT, `${name}.png`), Buffer.from(r.data, 'base64'));
|
||||
},
|
||||
close() { ws.close(); chrome.kill(); rmSync(profile, { recursive: true, force: true }); },
|
||||
// Teardown must not be able to fail the run. `chrome.kill()` returns as soon
|
||||
// as the signal is sent, so Chrome is often still writing its profile when
|
||||
// rmSync walks it, and macOS then throws ENOTEMPTY on a directory that
|
||||
// reappeared mid-delete. That throw used to propagate out of main's `finally`
|
||||
// and past the summary line, so the suite exited 2 and printed no verdict
|
||||
// whether it had passed or failed — which is how a stale assertion sat here
|
||||
// unnoticed. A leaked temp profile is worth less than a truthful exit code.
|
||||
close() {
|
||||
ws.close();
|
||||
chrome.kill();
|
||||
try {
|
||||
rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||
} catch (e) {
|
||||
console.log(` note could not remove the temp profile — ${e.code ?? e.message}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +177,7 @@ const PROBE = `(() => {
|
|||
let h = 2166136261;
|
||||
for (let i = 0; i < d.length; i += 4) { h ^= d[i] + d[i+1] * 31 + d[i+2] * 131; h = Math.imul(h, 16777619); }
|
||||
return {
|
||||
w: c.width, h: c.height, drawn, tones: [...tones].length,
|
||||
w: c.width, h: c.height, drawn, tones: [...tones].length, toneSet: [...tones],
|
||||
cx: drawn ? cx / drawn : null, cy: drawn ? cy / drawn : null,
|
||||
hash: h >>> 0,
|
||||
frame: document.querySelector('.readout span')?.textContent ?? '',
|
||||
|
|
@ -212,21 +235,24 @@ async function main() {
|
|||
// --- it is a mouth: two tones, one inside the other ---
|
||||
//
|
||||
// The three-layer structure is what makes a flat shape read as an opening
|
||||
// rather than a blob, so the interior being a SECOND tone is the check that
|
||||
// this is a mouth and not one polygon.
|
||||
// rather than a blob, so the interior being its OWN tone is the check that
|
||||
// this is a mouth and not one polygon. Asked of :mouth-dark specifically
|
||||
// rather than of the tone count: the count is now dominated by the eyes and
|
||||
// brows, which are on the stage whatever the mouth is doing.
|
||||
await page.eval(SEEK(10)); // beat 1: wide open
|
||||
await sleep(120);
|
||||
const open = await page.eval(PROBE);
|
||||
await page.shot('take-open');
|
||||
check(open.tones >= 2, 'an open mouth draws an outline and an interior',
|
||||
`${open.tones} tones`);
|
||||
check(open.toneSet.includes(MOUTH_DARK),
|
||||
'an open mouth draws an outline and an interior',
|
||||
`${open.tones} tones, interior ${open.toneSet.includes(MOUTH_DARK) ? 'present' : 'MISSING'}`);
|
||||
|
||||
await page.eval(SEEK(28)); // beat 3: shut
|
||||
await sleep(120);
|
||||
const shut = await page.eval(PROBE);
|
||||
await page.shot('take-shut');
|
||||
check(shut.tones === 1, 'a shut mouth draws the outline alone',
|
||||
`${shut.tones} tones`);
|
||||
check(!shut.toneSet.includes(MOUTH_DARK), 'a shut mouth draws the outline alone',
|
||||
`${shut.tones} tones, interior ${shut.toneSet.includes(MOUTH_DARK) ? 'STILL DRAWN' : 'gone'}`);
|
||||
check(open.hash !== shut.hash, 'the open and the shut mouth are different pictures');
|
||||
check(open.drawn > shut.drawn, 'the open mouth covers more of the stage',
|
||||
`${open.drawn} vs ${shut.drawn} px`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue