clone

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

main
miau-is-true / tests / world-card.test.js
world-card.test.js 5.1K
1import test from 'node:test';2import assert from 'node:assert/strict';3import { drawWorldCard, worldCardDescription, WORLD_CARD_COLORS } from '../src/render/world-card.js';45function context() {6 const calls = [];7 const ctx = {8 calls,9 canvas: { width: 640, height: 420 },10 font: '',11 save() { calls.push(['save']); },12 restore() { calls.push(['restore']); },13 beginPath() { calls.push(['beginPath']); },14 roundRect(...args) { calls.push(['roundRect', ...args]); },15 rect(...args) { calls.push(['rect', ...args]); },16 arc(...args) { calls.push(['arc', ...args]); },17 clip() { calls.push(['clip']); },18 fill() { calls.push(['fill', this.fillStyle]); },19 stroke() { calls.push(['stroke', this.strokeStyle]); },20 fillRect(...args) { calls.push(['fillRect', ...args, this.fillStyle]); },21 fillText(...args) { calls.push(['fillText', ...args]); },22 measureText(text) { return { width: String(text).length * 7 }; },23 };24 return ctx;25}2627test('drawWorldCard returns enter hit geometry and normalized display values', () => {28 const ctx = context();29 const result = drawWorldCard(ctx, {30 name: 'Tidewater', author: 'Dami', glyph: '🌊',31 tagline: "Everything floats until it doesn't.", accent: 'sink',32 number: 3, levelCount: 9, completedCount: 4,33 }, { width: 640, height: 420 });34 assert.deepEqual(result.world, {35 name: 'Tidewater', author: 'Dami', glyph: '🌊',36 tagline: "Everything floats until it doesn't.", accent: WORLD_CARD_COLORS.sink,37 number: '03', proofTotal: 9, completed: 4,38 meta: "9 proofs | ★ 4 / 9 | by Dami",39 });40 assert.ok(result.enter.width > 60);41 assert.equal(result.enter.height, 20);42 assert.ok(result.enter.x >= 0 && result.enter.x + result.enter.width <= 640);43 assert.ok(ctx.calls.some((call) => call[0] === 'fillText' && call[1] === '🌊'));44 assert.ok(ctx.calls.some((call) => call[0] === 'fillText' && call[1] === 'WORLD 03'));45 assert.ok(ctx.calls.some((call) => call[0] === 'fillText' && call[1] === result.world.meta));46 assert.ok(ctx.calls.some((call) => call[0] === 'stroke' && call[1] === WORLD_CARD_COLORS.sink));47});4849test('world card fits a narrow mobile canvas and wraps the tagline', () => {50 const ctx = context();51 const result = drawWorldCard(ctx, {52 name: 'A Very Long World Name', author: 'Dami', glyph: '🧭',53 tagline: 'Every proof turns the compass toward another room.', accent: 'tick',54 number: 2, levelCount: 3, completedCount: 1,55 }, { width: 390, height: 844, coarsePointer: true });56 assert.equal(result.card.x, 16);57 assert.equal(result.card.width, 358);58 assert.ok(result.card.y >= 16);59 assert.ok(result.enter.y + result.enter.height <= result.card.y + result.card.height);60 const lines = ctx.calls.filter((call) => call[0] === 'fillText').map((call) => call[1]);61 assert.ok(lines.length >= 7, 'mobile card should include wrapped tagline plus card copy');62 assert.ok(lines.some((line) => line.startsWith('Every proof turns')));63 assert.ok(lines.some((line) => line.endsWith('room.')));64 assert.equal(result.enter.height, 48, 'mobile action should be a 48px touch target');65 assert.equal(result.enter.width, result.card.width - 32, 'mobile action should span the card content width');66 assert.ok(lines.includes('ENTER WORLD'), 'mobile action should use the full button label');67 const metaCall = ctx.calls.find((call) => call[0] === 'fillText' && call[1] === result.world.meta);68 assert.ok(metaCall, 'mobile card should draw proof metadata');69 assert.ok(result.enter.y - metaCall[3] <= 48, 'mobile action should stay close to metadata');70});7172test('world card omits an absent byline while preserving proof metadata', () => {73 const ctx = context();74 const result = drawWorldCard(ctx, {75 name: 'No Author', glyph: '✦', tagline: 'A small proof.',76 number: 7, levelCount: 1, completedCount: 0,77 }, { width: 390, height: 844 });78 assert.equal(result.world.author, '');79 assert.equal(result.world.meta, '1 proof | ★ 0 / 1');80 const labels = ctx.calls.filter((call) => call[0] === 'fillText').map((call) => call[1]);81 assert.ok(labels.includes(result.world.meta));82 assert.ok(!labels.some((label) => String(label).includes('by ')));83});8485test('world-card narration shares the visible optional-author copy', () => {86 assert.equal(87 worldCardDescription({ name: 'Crossword', tagline: 'Read across and down.', author: 'World team' }),88 'Crossword. Read across and down. By World team. Press Enter to enter.',89 );90 assert.equal(worldCardDescription({ name: 'Uncredited' }), 'Uncredited. Press Enter to enter.');91});9293test('reduced motion settles the iris without clipping or changing card geometry', () => {94 const animated = drawWorldCard(context(), { name: 'Core', glyph: '🐈' }, {95 width: 640, height: 420, irisProgress: 0.25,96 });97 const reducedCtx = context();98 const reduced = drawWorldCard(reducedCtx, { name: 'Core', glyph: '🐈' }, {99 width: 640, height: 420, irisProgress: 0.25, reducedMotion: true,100 });101 assert.equal(animated.iris.progress, 0.25);102 assert.equal(reduced.iris.progress, 1);103 assert.deepEqual(reduced.card, animated.card);104 assert.equal(reducedCtx.calls.filter((call) => call[0] === 'clip').length, 0);105});