13 lines
4.8 KiB
JavaScript
13 lines
4.8 KiB
JavaScript
import {IdleWarmup} from './src/idle-warmup.mjs';
|
|
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import crypto from 'node:crypto';import {boot} from './qa/runtime-harness.mjs';import {CasualAudio} from './src/audio.mjs';import {solve,violations} from './src/upstream/rules.mjs';
|
|
const pack=JSON.parse(fs.readFileSync(new URL('./src/upstream/levels.json',import.meta.url))),levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
|
for(const drag of [false,true])test((drag?'drag':'undo button')+' reverses seven played steps as 7,6,5,4,3,2,1',()=>{const ui=boot();ui.select(3);const route=solve(levels[3])[0];for(const id of route.slice(0,7)){ui.advance(80);ui.cell(id);}const n=ui.played.length;for(let step=7;step>=1;step--){ui.advance(61);if(drag&&step>1)ui.cell(route[step-2]);else ui.button('撤销');assert.equal(ui.api.snapshot().path.length,step-1);}assert.deepEqual(Array.from(ui.played.slice(n)),[7,6,5,4,3,2,1].map(n=>'audio/move-'+n+'.wav'));ui.button('重开');assert.equal(ui.played.at(-1),'audio/restart.wav');});
|
|
function audioStub(){const played=[];globalThis.wx={createInnerAudioContext:()=>({stop(){},play(){played.push(this.src);},onError(){},onPlay(){},onEnded(){}})};return {played,sound:new CasualAudio({}, {getItem(){},setItem(){}},'test')};}
|
|
test('completion amplitude is reduced 15 percent while node, undo and restart volumes stay unchanged',()=>{const {sound}=audioStub();sound.play('complete');assert.equal(sound.channels[3].volume,.75*.85);sound.stop();sound.play('checkpoint',0,7);assert.equal(sound.channels[3].volume,.75);sound.play('undo',0,6);assert.equal(sound.channels[0].volume,.75);sound.play('restart');assert.equal(sound.channels[3].volume,.75);});
|
|
test('fast reverse coalesces to the latest descending pitch and cannot replay an old forward note',t=>{const now=Date.now,later=globalThis.setTimeout,cancel=globalThis.clearTimeout;let time=0,id=0;const jobs=new Map();Date.now=()=>time;globalThis.setTimeout=fn=>{jobs.set(++id,fn);return id;};globalThis.clearTimeout=i=>jobs.delete(i);t.after(()=>{Date.now=now;globalThis.setTimeout=later;globalThis.clearTimeout=cancel;});const {played,sound}=audioStub();sound.play('move',0,7);time=10;sound.play('undo',0,6);time=20;sound.play('undo',0,5);assert.equal(jobs.size,1);time=50;for(const [i,fn]of [...jobs]){jobs.delete(i);fn();}assert.deepEqual(played,['audio/move-7.wav','audio/move-6.wav']);time=60;sound.play('undo',0,4);sound.stop();assert.equal(jobs.size,0);});
|
|
test('all prepared routes match the immutable level hash and public rules',()=>{const data=JSON.parse(fs.readFileSync(new URL('./src/prepared-routes.json',import.meta.url)));assert.equal(data.sourceSHA256,crypto.createHash('sha256').update(fs.readFileSync(new URL('./src/upstream/levels.json',import.meta.url))).digest('hex'));assert.equal(Object.keys(data.routes).length,200);for(const l of levels){const path=data.routes[l.id];assert.equal(path.length,l.cells.length);assert.equal(new Set(path).size,path.length);assert.equal(violations(l,path).length,0);}});
|
|
test('entering next level draws the live board once and preserves the running BGMA player',()=>{const ui=boot();for(const i of [0,89,121,143,161,180,190]){ui.select(i);solve(levels[i])[0].forEach(ui.cell);const before=ui.api.snapshot().boardRenders,loop=ui.played.filter(f=>f==='music/play-a.wav').length;ui.button('下一关 →');assert.equal(ui.api.snapshot().displayOrder,i+2);assert.equal(ui.api.snapshot().boardRenders-before,1);assert.equal(ui.played.filter(f=>f==='music/play-a.wav').length,loop);}});
|
|
|
|
test('next-board warming advances one item per frame and cancels on deadline, replacement or hide',()=>{let now=0,id=0,allowed=true,steps=0;const jobs=new Map(),w=new IdleWarmup({after:fn=>{jobs.set(++id,fn);return id;},cancel:id=>jobs.delete(id),now:()=>now,canRun:()=>allowed});const run=()=>{const callbacks=[...jobs.values()];jobs.clear();for(const fn of callbacks)fn();};function* work(){for(let i=0;i<3;i++){steps++;yield;}}w.start('a',work());run();assert.equal(steps,1);allowed=false;run();assert.equal(steps,1);allowed=true;run();assert.equal(steps,2);w.stop();assert.equal(jobs.size,0);w.start('b',work());now=801;run();assert.equal(steps,2);assert.equal(w.pending,false);now=1000;w.start('c',work());w.start('d',work());run();assert.equal(steps,3);w.stop();});
|
|
test('idle material warming never changes the player path or emits sound and vibration',()=>{const ui=boot();ui.select(3);const before=ui.api.snapshot(),sounds=ui.played.length,pulses=ui.vibrated.length;for(let i=0;i<40;i++)ui.advance(16);assert.deepEqual(Array.from(ui.api.snapshot().path),Array.from(before.path));assert.equal(ui.played.length,sounds);assert.equal(ui.vibrated.length,pulses);assert.equal(ui.api.snapshot().feedback.sequence,before.feedback.sequence);ui.handlers.Hide();assert.equal(ui.frames.size,0);});
|