clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / tests / stage-hud.test.js
stage-hud.test.js 5.0K
1import test from 'node:test';2import assert from 'node:assert/strict';3import { drawAutomataStrip, drawInlineActions, drawLedger, drawStepCounter } from '../src/render/stage-hud.js';4import { BOARD_COLORS } from '../src/render/board/model.js';56function context() {7 const calls = [];8 const ctx = {9 calls,10 font: '',11 save() { calls.push(['save']); },12 restore() { calls.push(['restore']); },13 translate(...args) { calls.push(['translate', ...args]); },14 scale(...args) { calls.push(['scale', ...args]); },15 beginPath() { calls.push(['beginPath']); },16 moveTo(...args) { calls.push(['moveTo', ...args]); },17 lineTo(...args) { calls.push(['lineTo', ...args]); },18 roundRect(...args) { calls.push(['roundRect', ...args]); },19 rect(...args) { calls.push(['rect', ...args]); },20 clip() { calls.push(['clip']); },21 stroke() { calls.push(['stroke']); },22 strokeRect(...args) { calls.push(['strokeRect', ...args]); },23 fillRect(...args) { calls.push(['fillRect', ...args, this.fillStyle]); },24 fillText(...args) { calls.push(['fillText', ...args]); },25 measureText(text) { return { width: String(text).length * 6 }; },26 };27 Object.defineProperty(ctx, 'fillStyle', {28 get() { return this._fillStyle; },29 set(value) { this._fillStyle = value; },30 });31 return ctx;32}3334test('drawLedger uses the board palette for every core property tile', () => {35 const ctx = context();36 const properties = ['YOU', 'PUSH', 'STOP', 'WIN', 'SINK', 'DEFEAT', 'TICK'];37 const entries = properties.map((property) => ({ text: `MIAU IS ${property}` }));38 drawLedger(39 ctx,40 entries,41 10,42 20,43 2000,44 );45 const tileFills = ctx.calls46 .filter((call) => call[0] === 'fillRect' && call[5] !== '#000000')47 .map((call) => call[5]);48 const expected = properties.map((property) => BOARD_COLORS[property]);49 const propertyFills = tileFills.filter((fill) => expected.includes(fill));50 assert.equal(propertyFills.length, expected.length + 1, 'only the fallback PUSH tile adds a second core fill');51 assert.deepEqual(propertyFills.slice(0, expected.length), expected);52 assert.equal(propertyFills.at(-1), BOARD_COLORS.PUSH);53});5455test('drawLedger consumes physical frame entries with stamp, strike, and exit state', () => {56 const ctx = context();57 drawLedger(ctx, [{ text: 'MIAU IS YOU' }], 10, 20, 300, {58 ledger: [{59 key: 'a>b>c',60 ids: ['a', 'b', 'c'],61 tokens: ['MIAU', 'IS', 'YOU'],62 stampProgress: [0.5, 0, 0],63 strikeProgress: 0.5,64 exitProgress: 0,65 }],66 });67 assert.equal(ctx.calls.filter(call => call[0] === 'fillText').length, 4);68 assert.ok(ctx.calls.some(call => call[0] === 'stroke'), 'strike should draw after the stamped tile');69 assert.ok(ctx.calls.some(call => call[0] === 'scale' && call[1] > 1), 'stamped tile should land from scale 1.3');70});7172test('drawLedger falls back to settled rules when no frame ledger exists', () => {73 const ctx = context();74 drawLedger(ctx, [{ text: 'MIAU IS YOU' }], 10, 20, 300);75 assert.equal(ctx.calls.filter(call => call[0] === 'fillText').length, 6);76});7778test('drawAutomataStrip uses one actor tile, directional beat cells, and a sentence label', () => {79 const ctx = context();80 const result = drawAutomataStrip(ctx, [{81 subject: 'BELL',82 property: 'TICK',83 phase: 0,84 active: true,85 pattern: [{ dx: -1, action: 'move' }, { action: 'wait' }, { dx: 1, action: 'move' }],86 }], 10, 20, 300, [], { viewportWidth: 390, palette: { BELL: '🔔' } });87 const text = ctx.calls.filter((call) => call[0] === 'fillText').map((call) => call[1]);88 assert.ok(text.includes('🔔'));89 assert.ok(text.includes('←'));90 assert.ok(text.includes('‖'));91 assert.ok(text.includes('BELL IS TICK'));92 assert.equal(text.includes('ACTIVE'), false);93 const beatRects = ctx.calls.filter((call) => call[0] === 'fillRect' && call[3] === 22);94 assert.equal(beatRects.length, 3);95 assert.equal(beatRects[0][5], '#C6F432', 'current board cue is the filled target');96 assert.equal(beatRects[1][5], '#191816', 'other beats stay in the outlined trail');97 assert.equal(result.height, 24);98});99100test('drawInlineActions presses only the matching Win action keycap', () => {101 const ctx = context();102 drawInlineActions(ctx, [103 { key: 'ENTER', label: 'continue', action: 'confirm' },104 { key: 'Z', label: 'undo', action: 'undo' },105 ], 160, 40, {106 now: 30,107 keycapPress: { action: 'confirm', startedAt: 0 },108 });109 // The pressed keycap loses its lip fill while the other keeps both faces.110 assert.equal(ctx.calls.filter(call => call[0] === 'fillRect').length, 3);111});112113test('odometer clips each rolling digit to one text box', () => {114 const ctx = context();115 drawStepCounter(ctx, 8, 100, 39, {116 now: 60,117 roll: { from: 7, to: 8, startedAt: 0, duration: 120 },118 });119 const clips = ctx.calls.filter(call => call[0] === 'rect');120 assert.equal(clips.length, 2);121 assert.ok(clips.every(call => call[2] === 22 && call[4] === 34));122 assert.equal(ctx.calls.filter(call => call[0] === 'clip').length, 2);123});