Files
functional-programming-snake/js/test/m09-app.test.js
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

84 lines
4.1 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { states, classicActions, render, main, play, TICK_MS } from '../src/app.js'
import { run } from '../src/stream.js'
import { trace } from '../src/io.js'
import { view } from '../src/view.js'
import { newGame } from '../src/spawn.js'
import { Action } from '../src/model.js'
import { history } from '../src/game.js'
import { freeze } from './helpers.js'
import { classicMain } from './m09-helpers.js'
const time = (t) => ({ tag: 'Time', t })
const down = (key) => ({ tag: 'KeyDown', key })
test('m09 the game ticks every 150 ms', () => assert.equal(TICK_MS, 150))
test('m09 classicActions turns Tick into actions on the clock and keys into Turn actions', () =>
assert.deepStrictEqual(
run(classicActions, freeze([time(150), down('ArrowUp'), down('x'), time(300)])),
[Action.Tick, Action.Turn('Up'), Action.Tick]))
test('m09 states starts with the new game, then yields one state per action', () => {
const out = run(states(42, classicActions), freeze([time(150), down('ArrowUp'), time(300)]))
assert.equal(out.length, 4)
assert.deepStrictEqual(out[0], newGame(42))
assert.deepStrictEqual(out.map((s) => s.snake[0]), [
{ x: 10, y: 10 }, { x: 11, y: 10 }, { x: 11, y: 10 }, { x: 11, y: 9 },
])
assert.deepStrictEqual(out.map((s) => s.nextDir), ['Right', 'Right', 'Up', 'Up'])
assert.deepStrictEqual(out.map((s) => s.dir), ['Right', 'Right', 'Right', 'Up'])
})
test('m09 render draws the whole view of the state', () =>
assert.deepStrictEqual(trace(render(newGame(42)), 0).ops, view(newGame(42)).map((shape) => ({ tag: 'Draw', shape }))))
// Module 10 (Exercise 10.7) makes `main` also load the stored best and draw one more shape after the game's view,
// so this test accepts both versions; m10 pins the final one exactly.
test('m09 main asks for a random seed, then builds the stream from it', () => {
const { ops, result: stream } = trace(main, 42)
assert.deepStrictEqual(ops[0], { tag: 'RandomSeed' })
assert.ok(ops.length === 1 || (ops.length === 2 && ops[1].tag === 'LoadBest'),
`main may only ask for the seed (and, from m10 on, the stored best); it asked for ${JSON.stringify(ops)}`)
const [outs] = stream.step(time(0))
assert.equal(outs.length, 1)
const expected = trace(render(newGame(42)), 0).ops
assert.deepStrictEqual(trace(outs[0], 0).ops.slice(0, expected.length), expected)
})
// `main` itself switches to speedActions in m10, so the 150 ms behaviour is checked on the same program built from classicActions.
test('m09 the program built from classicActions draws the new game first, then the ticked game after 150 ms', () => {
const { result: stream } = trace(classicMain, 42)
const [first, next] = stream.step(time(0))
const [second] = next.step(time(150))
assert.deepStrictEqual(trace(first[0], 0).ops.map((o) => o.shape), view(newGame(42)))
assert.equal(second.length, 1)
assert.notDeepEqual(trace(second[0], 0).ops, trace(first[0], 0).ops)
})
// Holds before and after m10: whatever `main` ticks with, the clock must be wired in, so time passing produces more drawings.
test('m09 main draws again once time has passed', () => {
const { result: stream } = trace(main, 42)
const [first, next] = stream.step(time(0))
const [later] = next.step(time(150))
assert.equal(first.length, 1)
assert.ok(later.length >= 1)
})
// ---- play: the whole program in one equality (Exercise 9.5)
const drawOps = (state) => view(state).map((shape) => ({ tag: 'Draw', shape }))
test('m09 play on the classic program equals the scenes of the expected states', () => {
const events = freeze([time(150), down('ArrowUp'), down('x'), time(300), time(310)])
const expectedStates = history(newGame(42), [Action.Tick, Action.Turn('Up'), Action.Tick])
assert.deepStrictEqual(play(classicMain, 42, events), expectedStates.map(drawOps))
})
test('m09 play with no events gives no drawings', () =>
assert.deepStrictEqual(play(classicMain, 42, []), []))
test('m09 play uses the seed for the program\'s RandomSeed', () =>
assert.deepStrictEqual(play(classicMain, 7, freeze([time(0)])), [drawOps(newGame(7))]))