clone

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

main
miau-is-true / tests / catalog.test.js
catalog.test.js 10.3K
1import test from 'node:test';2import assert from 'node:assert/strict';3import { createCatalog } from '../src/catalog.js';45const pack = (id, title, levels) => ({6 manifest: { id, name: title, version: '1.0.0' },7 palette: { MIAU: '🐈' },8 levels,9});10const level = (title, id) => ({ id, title, width: 2, height: 1, objects: [11 { kind: 'word', text: 'MIAU', x: 0, y: 0 },12 { kind: 'word', text: 'IS', x: 1, y: 0 },13] });1415test('starts with stable core pack and preserves additive import order', () => {16 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });17 const core = catalog.getPack('core');18 assert.equal(core.manifest.rules, 2);19 assert.equal(core.manifest.author, 'Miau Is True');20 assert.deepEqual(core.manifest.world, { glyph: '🐈', tagline: 'Where the rules first bend.', accent: 'you' });21 assert.deepEqual(catalog.getPacks()[0].world, core.manifest.world);22 assert.deepEqual(catalog.getPacks().map(pack => pack.id), ['core']);23 catalog.importMod(pack('alpha', 'Alpha', [level('A one', 'start'), level('A two', 'next')]));24 catalog.importMod(pack('beta', 'Beta', [level('B one', 'start')]));25 assert.deepEqual(catalog.getPacks().map(pack => pack.id), ['core', 'alpha', 'beta']);26 assert.equal(catalog.getPacks().find(pack => pack.id === 'alpha').origin, 'local');27 assert.deepEqual(catalog.getLevels().map(level => level.id), ['core:intro', 'alpha:start', 'alpha:next', 'beta:start']);28 assert.equal(catalog.getLevel('alpha:start').origin, 'local');29});3031test('core world identity remains effective through catalog normalization', () => {32 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });33 const [core] = catalog.getPacks();34 assert.equal(core.id, 'core');35 assert.equal(core.manifest.author, 'Miau Is True');36 assert.deepEqual(core.world, {37 glyph: '🐈',38 tagline: 'Where the rules first bend.',39 accent: 'you',40 });41 assert.deepEqual(core.worldWarnings, []);42});4344test('invalid world identity falls back without losing authored pack data', () => {45 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });46 const source = pack('world-bad', 'World Bad', [level('Proof', 'intro')]);47 source.palette = { MIAU: '🐈' };48 source.manifest.world = { glyph: '🐈🐈', tagline: 'too long'.repeat(20), accent: 'glow' };49 catalog.register(source, { origin: 'local' });50 const record = catalog.getPacks().find(item => item.id === 'world-bad');51 assert.deepEqual(record.world, { glyph: '🐈', tagline: null, accent: null });52 assert.equal(record.worldWarnings.length, 3);53 assert.deepEqual(catalog.getPack('world-bad').manifest.world, source.manifest.world);54});5556test('invalid gameplay still rejects a pack with world metadata atomically', () => {57 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });58 const source = pack('world-invalid-gameplay', 'World Invalid Gameplay', [{ ...level('Bad', 'intro'), objects: [{ kind: 'object', noun: 'MIAU', x: 4, y: 0 }] }]);59 source.manifest.world = { glyph: '🐈🐈', accent: 'glow' };60 assert.throws(() => catalog.register(source), /outside the 2x1 board/);61 assert.equal(catalog.getPack('world-invalid-gameplay'), undefined);62});6364test('supports ordered and ID based level lookup plus active selection', () => {65 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });66 catalog.register(pack('alpha', 'Alpha', [level('A one', 'start')]));67 assert.equal(catalog.getLevel(1).id, 'alpha:start');68 assert.equal(catalog.getLevel('alpha:start').title, 'A one');69 assert.equal(catalog.getActiveLevel().id, 'core:intro');70 assert.equal(catalog.setActiveLevel('alpha:start'), true);71 assert.equal(catalog.activeLevelId, 'alpha:start');72 assert.equal(catalog.getActiveLevel().packId, 'alpha');73 assert.equal(catalog.setActiveLevel('missing'), false);74 assert.equal(catalog.getLevel('missing'), undefined);75});7677test('returns the validated original pack for export without exposing registry state', () => {78 const source = pack('alpha', 'Alpha', [level('A one', 'start')]);79 source.vocabulary = ['MIAU'];80 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });81 catalog.register(source);82 const exported = catalog.getPack('alpha');83 assert.equal(exported.manifest.id, 'alpha');84 assert.equal(exported.levels[0].id, 'start');85 assert.deepEqual(exported.palette, source.palette);86 assert.equal(exported.origin, undefined);87 exported.manifest.name = 'Changed';88 exported.levels[0].title = 'Changed';89 assert.equal(catalog.getPack('alpha').manifest.name, 'Alpha');90 assert.equal(catalog.getPack('alpha').levels[0].title, 'A one');91 assert.equal(catalog.getPack('missing'), undefined);92});9394test('rejects duplicate pack IDs and leaves the earlier pack intact', () => {95 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });96 catalog.register(pack('alpha', 'Original', [level('Original', 'one')]));97 assert.throws(() => catalog.register(pack('alpha', 'Replacement', [level('Replacement', 'two')])), /pack id collision: alpha/);98 assert.equal(catalog.getPacks().length, 2);99 assert.equal(catalog.getLevel('alpha:one').title, 'Original');100});101102test('replaces an imported pack atomically, preserving order and repairing active selection', () => {103 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });104 catalog.register(pack('alpha', 'Original', [level('Original one', 'one'), level('Original two', 'two')]), { origin: 'url' });105 catalog.setActiveLevel('alpha:two');106 catalog.register(pack('alpha', 'Replacement', [level('Replacement', 'new')]), { origin: 'url', replace: true });107 assert.deepEqual(catalog.getPacks().map(pack => pack.id), ['core', 'alpha']);108 assert.equal(catalog.getPacks()[1].origin, 'url');109 assert.equal(catalog.getLevel('alpha:new').origin, 'url');110 assert.equal(catalog.activeLevelId, 'alpha:new');111 assert.equal(catalog.getLevel('alpha:two'), undefined);112});113114test('replacement preserves the active stable ID when its authored level remains', () => {115 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });116 catalog.register(pack('alpha', 'Original', [level('Original', 'one'), level('Other', 'two')]));117 catalog.setActiveLevel('alpha:two');118 catalog.register(pack('alpha', 'Replacement', [level('Updated', 'two'), level('New', 'three')]), { replace: true });119 assert.equal(catalog.activeLevelId, 'alpha:two');120 assert.equal(catalog.getActiveLevel().title, 'Updated');121 assert.equal(catalog.getLevel(2).id, 'alpha:three');122});123124test('protects the bundled core pack from replacement', () => {125 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });126 assert.throws(() => catalog.register(pack('core', 'Fake Core', [level('Fake', 'fake')]), { replace: true }), /bundled and cannot be replaced/);127 assert.equal(catalog.getPacks()[0].origin, 'bundled');128 assert.equal(catalog.getLevel('core:intro').title, 'Core');129});130131test('removes imported packs, preserves order, and repairs active selection', () => {132 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });133 catalog.register(pack('alpha', 'Alpha', [level('Alpha', 'alpha')]));134 catalog.register(pack('beta', 'Beta', [level('Beta', 'beta')]));135 catalog.setActiveLevel('beta:beta');136 assert.equal(catalog.removePack('beta'), true);137 assert.deepEqual(catalog.getPacks().map(pack => pack.id), ['core', 'alpha']);138 assert.equal(catalog.getLevel('beta:beta'), undefined);139 assert.equal(catalog.activeLevelId, 'core:intro');140 assert.equal(catalog.removePack('missing'), false);141});142143test('protects core and bundled packs from removal', () => {144 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });145 assert.throws(() => catalog.removePack('core'), /bundled pack cannot be removed/);146 catalog.register(pack('bundled-pack', 'Bundled', [level('Bundled', 'only')]), { origin: 'bundled' });147 assert.throws(() => catalog.removePack('bundled-pack'), /bundled pack cannot be removed/);148 assert.deepEqual(catalog.getPacks().map(pack => pack.id), ['core', 'bundled-pack']);149});150151test('rejects duplicate local level IDs before registration', () => {152 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });153 assert.throws(() => catalog.register(pack('alpha', 'Alpha', [level('One', 'same'), level('Two', 'same')])), /duplicates another level id/);154 assert.deepEqual(catalog.getPacks().map(pack => pack.id), ['core']);155});156157test('requires authored level IDs and a known registration origin', () => {158 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });159 const missingId = pack('missing-id', 'Missing ID', [{ ...level('No ID', 'temporary'), id: undefined }]);160 assert.throws(() => catalog.register(missingId), /levels\[0\]\.id/);161 assert.throws(() => catalog.register(pack('origin', 'Origin', [level('Origin', 'origin')]), { origin: 'remote' }), /origin must be one of/);162 assert.deepEqual(catalog.getPacks().map(pack => pack.id), ['core']);163});164165test('validates a module before registering any of its levels', () => {166 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });167 const invalid = pack('broken', 'Broken', [{ ...level('Bad', 'bad'), objects: [{ kind: 'object', noun: 'YOU', x: 0, y: 0 }] }]);168 assert.throws(() => catalog.importMod(invalid), /levels\[0\]\.objects\[0\]\.noun/);169 assert.equal(catalog.size, 1);170 assert.equal(catalog.getLevel('broken:bad'), undefined);171});172173test('returns cloned levels so callers cannot mutate registry state', () => {174 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });175 const first = catalog.getLevel('core:intro');176 first.title = 'Changed';177 first.objects[0].text = 'BROKEN';178 assert.equal(catalog.getLevel('core:intro').title, 'Core');179 assert.equal(catalog.getLevel('core:intro').objects[0].text, 'MIAU');180});181182test('pack summaries are invalidated and returned as caller-owned clones', () => {183 const catalog = createCatalog({ coreLevels: [level('Core', 'intro')] });184 const first = catalog.getPacks();185 first[0].manifest.name = 'Changed';186 catalog.register(pack('alpha', 'Alpha', [level('A', 'a')]));187 assert.equal(catalog.getPacks()[0].manifest.name, 'Miau Core');188 catalog.register(pack('alpha', 'Replaced', [level('B', 'b'), level('C', 'c')]), { replace: true });189 assert.equal(catalog.getPacks().find(pack => pack.id === 'alpha').manifest.name, 'Replaced');190 assert.equal(catalog.getPacks().find(pack => pack.id === 'alpha').levelCount, 2);191});