Files
functional-programming-snake/js/test/m09-runtime.test.js
T

118 lines
4.0 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { runIO, runApp } from '../src/runtime.js'
import { pure, draw, randomSeed, map, sequence } from '../src/io.js'
import { states, classicActions, render } from '../src/app.js'
import { map as mapStream } from '../src/stream.js'
import { view } from '../src/view.js'
import { newGame, tick } from '../src/spawn.js'
// A fake platform: records every call and lets the test fire events by hand.
const fakePlatform = (seed = 42) => {
const calls = []
const handlers = {}
return {
calls,
fillRect: (...args) => calls.push(['fillRect', ...args]),
fillText: (...args) => calls.push(['fillText', ...args]),
randomSeed: () => { calls.push(['randomSeed']); return seed },
startClock: (cb) => { handlers.time = cb },
onKeyDown: (cb) => { handlers.down = cb },
onKeyUp: (cb) => { handlers.up = cb },
fireTime: (t) => handlers.time(t),
fireKey: (k) => handlers.down(k),
fireKeyUp: (k) => handlers.up(k),
}
}
// The m09 program: ticks every 150 ms. (The shipped `main` uses speedActions after m10, so these tests build their own.)
const main = map(randomSeed, (seed) => mapStream(states(seed, classicActions), render))
// What the platform should have been asked to draw for a scene.
const expectedCalls = (scene) =>
scene.map((s) =>
s.tag === 'Rect'
? ['fillRect', s.x, s.y, s.w, s.h, s.color]
: ['fillText', s.x, s.y, s.text, s.color, s.size, s.align])
const rect = { tag: 'Rect', x: 1, y: 2, w: 3, h: 4, color: '#abcdef' }
const text = { tag: 'Text', x: 5, y: 6, text: 'hi', color: '#fff', size: 16, align: 'center' }
test('m09 runIO of a pure value returns it without touching the platform', () => {
const p = fakePlatform()
assert.equal(runIO(pure(7), p), 7)
assert.deepStrictEqual(p.calls, [])
})
test('m09 runIO maps Draw Rect to fillRect(x, y, w, h, color)', () => {
const p = fakePlatform()
runIO(draw(rect), p)
assert.deepStrictEqual(p.calls, [['fillRect', 1, 2, 3, 4, '#abcdef']])
})
test('m09 runIO maps Draw Text to fillText(x, y, text, color, size, align)', () => {
const p = fakePlatform()
runIO(draw(text), p)
assert.deepStrictEqual(p.calls, [['fillText', 5, 6, 'hi', '#fff', 16, 'center']])
})
test('m09 runIO answers RandomSeed with platform.randomSeed() and continues with it', () => {
const p = fakePlatform(99)
assert.equal(runIO(map(randomSeed, (s) => s + 1), p), 100)
assert.deepStrictEqual(p.calls, [['randomSeed']])
})
test('m09 runIO performs effects in order', () => {
const p = fakePlatform()
runIO(sequence([draw(rect), draw(text), draw(rect)]), p)
assert.deepStrictEqual(p.calls.map((c) => c[0]), ['fillRect', 'fillText', 'fillRect'])
})
test('m09 runIO handles 100000 effects without overflowing the stack', () => {
const p = fakePlatform()
const n = 100_000
runIO(sequence(Array.from({ length: n }, () => draw(rect))), p)
assert.equal(p.calls.length, n)
})
test('m09 runApp asks the platform for a seed once, at start, and draws nothing before the first input', () => {
const p = fakePlatform()
runApp(main, p)
assert.deepStrictEqual(p.calls, [['randomSeed']])
})
test('m09 runApp draws the initial scene on the first clock tick, then the ticked scene after 150 ms', () => {
const p = fakePlatform(42)
runApp(main, p)
p.calls.length = 0
p.fireTime(150)
assert.deepStrictEqual(p.calls, [
...expectedCalls(view(newGame(42))),
...expectedCalls(view(tick(newGame(42)))),
])
})
test('m09 runApp feeds key presses to the game', () => {
const p = fakePlatform(42)
runApp(main, p)
p.fireTime(0)
p.calls.length = 0
p.fireKey('ArrowUp')
p.fireTime(150)
const turned = { ...newGame(42), nextDir: 'Up' }
assert.deepStrictEqual(p.calls, [
...expectedCalls(view(turned)),
...expectedCalls(view(tick(turned))),
])
})
test('m09 runApp ignores key releases and unmapped keys (no redraw)', () => {
const p = fakePlatform(42)
runApp(main, p)
p.fireTime(0)
p.calls.length = 0
p.fireKeyUp('ArrowUp')
p.fireKey('x')
assert.deepStrictEqual(p.calls, [])
})