Audit pass: - Frame rule counts frames since the last move, so every step lasts exactly movesEvery(score) frames, also right after a speed-up - clampTime + MAX_GAP_MS: a hidden tab no longer replays the whole gap - Pong Restart keeps held keys - Fact corrections (mulberry32 output coverage, Haskell IO, uniform) - New fixture cases from a mutation analysis Extensions (all three tracks, shared fixtures, course text): - Turn queue (Exercise 10.6), persisted high score (10.7), replay viewer (10.8), play() end-to-end helper (9.5) - Optional State (4.7) and optional Module 7b, stack-safe IO - Module 12, property-based testing with generators, shrinking, invariants and a model-based oracle - Module 13, Conway's Life with a Click event - JS type checking via jsconfig, JSDoc and types.d.ts - Maintainer tools: scripts/mutate.mjs, scripts/typecheck-js.sh, Playwright end-to-end suite (e2e/, scripts/e2e.sh) Docs: docs/contract.md is the canonical contract; specs and build log under docs/superpowers. Verified: JS 728, Scala 689, F# 691 unit tests; stubs and progression M01-M13 and M07b; 176 end-to-end tests on Chromium and WebKit. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
68 lines
3.7 KiB
JavaScript
68 lines
3.7 KiB
JavaScript
// Technique 3: recorded drawing.
|
|
// The glue of every track draws a Rect shape as `fillStyle = color; fillRect(x, y, w, h)` and a Text shape as
|
|
// `fillStyle = color; font = "<size>px monospace"; textAlign = align; fillText(text, x, y)`. This init script wraps
|
|
// fillRect and fillText on CanvasRenderingContext2D.prototype before any page script runs and records each call with
|
|
// the fill style, font and alignment in force at the moment of the call, read back from the context (so colours and
|
|
// fonts are recorded the way the canvas reports them, e.g. "#7cb342" and "16px monospace").
|
|
//
|
|
// Frames. A frame is everything drawn in one go: in all three runtimes one input (an animation frame, a key, a click)
|
|
// is fed to the program and every drawing it produces is performed synchronously before the callback returns. So the
|
|
// first call after a quiet spell opens a frame (stamped with the fake `performance.now()` of that moment) and a
|
|
// queued microtask, which runs as soon as the callback has returned, closes it. One frame of the page therefore
|
|
// corresponds to the drawings the model produces for one input; if an input produces several drawings (several game
|
|
// frames due in one animation frame), they are all in that one page frame, in order.
|
|
|
|
/** Runs in the page. */
|
|
const recorderScript = () => {
|
|
const frames = []
|
|
let open = null
|
|
const current = () => {
|
|
if (open === null) {
|
|
open = { at: performance.now(), shapes: [] }
|
|
frames.push(open)
|
|
queueMicrotask(() => { open = null })
|
|
}
|
|
return open
|
|
}
|
|
const proto = CanvasRenderingContext2D.prototype
|
|
const fillRect = proto.fillRect
|
|
const fillText = proto.fillText
|
|
proto.fillRect = function (x, y, w, h) {
|
|
current().shapes.push({ tag: 'Rect', x, y, w, h, color: this.fillStyle })
|
|
return fillRect.call(this, x, y, w, h)
|
|
}
|
|
proto.fillText = function (text, x, y, ...rest) {
|
|
current().shapes.push({ tag: 'Text', x, y, text: String(text), color: this.fillStyle, font: this.font, align: this.textAlign })
|
|
return fillText.call(this, text, x, y, ...rest)
|
|
}
|
|
Object.defineProperty(window, '__e2eFrames', { value: frames })
|
|
}
|
|
|
|
/** Install the recorder for every document the page loads from now on. */
|
|
export const recordDrawing = (page) => page.addInitScript(recorderScript)
|
|
|
|
/** The frames recorded in the current document from index `from` on: [{at, shapes}]. */
|
|
export const framesFrom = (page, from = 0) => page.evaluate((i) => (window.__e2eFrames ?? []).slice(i), from)
|
|
|
|
/** How many frames the current document has recorded. */
|
|
export const frameCount = (page) => page.evaluate(() => (window.__e2eFrames ?? []).length)
|
|
|
|
/** Ask the page's canvas how it reports each colour (fillStyle normalises "#ABC", names and so on). */
|
|
export const canvasColors = (page, colors) => page.evaluate((cs) => {
|
|
const ctx = document.createElement('canvas').getContext('2d')
|
|
return cs.map((c) => { ctx.fillStyle = '#000000'; ctx.fillStyle = c; return ctx.fillStyle })
|
|
}, colors)
|
|
|
|
/** A recorded Text shape, with its font split into size and family, so it can be compared with a model shape. */
|
|
export const pageShape = (s) => {
|
|
if (s.tag !== 'Text') return s
|
|
const m = /^\s*(?:\S+\s+)*?(\d+(?:\.\d+)?)px\s+(.+?)\s*$/.exec(s.font)
|
|
return { tag: 'Text', x: s.x, y: s.y, text: s.text, color: s.color, size: m ? Number(m[1]) : s.font, family: m ? m[2] : s.font, align: s.align }
|
|
}
|
|
|
|
/** A model shape as the page should have recorded it; `color` maps a model colour to the canvas's spelling. */
|
|
export const expectedShape = (s, color) =>
|
|
s.tag === 'Rect'
|
|
? { tag: 'Rect', x: s.x, y: s.y, w: s.w, h: s.h, color: color(s.color) }
|
|
: { tag: 'Text', x: s.x, y: s.y, text: s.text, color: color(s.color), size: s.size, family: 'monospace', align: s.align }
|