Files
functional-programming-snake/e2e/lib/drawing.mjs
T
Lucas WintherandClaude Fable 5.2 edd57f0148 feat(course): audit fixes and twelve extensions (Modules 7b, 12, 13)
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>
2026-11-11 12:05:29 +01:00

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 }