Files
NumberSwipe_New/wechat-campaign-200-r2/resume-select.test.mjs
T

61 lines
5.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {boot} from './qa/runtime-harness.mjs';
import {solve,canAppend} from './src/upstream/rules.mjs';
import {RESUME_KEY,resumeTarget,validResumePath} from './src/resume.mjs';
import {chapterLayout} from './src/level-select.mjs';
import {prepareHintMotion,hintPointAt} from './src/hints.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));
const closeRules=ui=>{if(ui.api.buttons().some(b=>b.label==='返回游戏'))ui.button('返回游戏');};
test('first launch goes directly to gameplay',()=>{const ui=boot();assert.equal(ui.api.snapshot().screen,'play');assert.equal(ui.api.snapshot().displayOrder,1);});
for(const exitFromSelect of [false,true])test('resume unfinished route after exit from '+(exitFromSelect?'selection':'gameplay'),()=>{
const ui=boot();ui.select(3);const route=solve(levels[3])[0].slice(0,3);route.forEach(ui.cell);
if(exitFromSelect)ui.button('选关');ui.handlers.Hide();
ui.handlers.Show();ui.tick();assert.equal(ui.api.snapshot().screen,'play','warm re-entry also goes to gameplay');assert.deepEqual(Array.from(ui.api.snapshot().path),route);
const restored=boot(ui.storage);assert.equal(restored.api.snapshot().screen,'play');assert.equal(restored.api.snapshot().displayOrder,4);assert.deepEqual(Array.from(restored.api.snapshot().path),route);
assert.ok(restored.api.buttons().some(b=>b.label==='撤销'&&b.enabled));restored.button('撤销');assert.equal(restored.api.snapshot().path.length,2);
restored.button('重开');const clean=boot(restored.storage);assert.equal(clean.api.snapshot().path.length,0);
});
test('completed level resumes next unfinished, including leaving from cleared screen',()=>{
const ui=boot();ui.select(0);solve(levels[0])[0].forEach(ui.cell);ui.handlers.Hide();const restored=boot(ui.storage);assert.equal(restored.api.snapshot().displayOrder,2);assert.equal(restored.api.snapshot().screen,'play');assert.equal(restored.api.snapshot().path.length,0);
});
test('replaying a cleared level does not replace the unfinished save',()=>{
const store=new Map([['variant:campaign-200-r2:v1',JSON.stringify([levels[0].id])]]),ui=boot(store);ui.select(3);solve(levels[3])[0].slice(0,2).forEach(ui.cell);const saved=ui.storage.get(RESUME_KEY);ui.select(0);ui.cell(solve(levels[0])[0][0]);ui.button('选关');ui.handlers.Hide();assert.equal(ui.storage.get(RESUME_KEY),saved);assert.equal(boot(store).api.snapshot().displayOrder,4);
});
test('legacy saves, malformed data and full completion have valid startup targets',()=>{
assert.equal(resumeTarget(levels,[levels[3].id],null,levels[3].id).index,4);
assert.equal(resumeTarget(levels,levels.map(l=>l.id),null,levels[199].id).index,199);
assert.deepEqual(validResumePath(levels[0],['missing'],canAppend),[]);
for(const value of ['broken','{}','{"version":1,"levelId":"missing","path":[]}']){const ui=boot(new Map([[RESUME_KEY,value]]));assert.equal(ui.api.snapshot().screen,'play');assert.equal(ui.api.snapshot().displayOrder,1);}
});
test('chapter pages expose at most ten cards and fit phone safe areas',()=>{
for(const [w,h]of [[320,568],[375,667],[390,844],[414,896]]){
const ui=boot(new Map(),w,h);closeRules(ui);ui.button('选关');
for(let chapter=1;chapter<=20;chapter++){
const cards=ui.api.buttons().filter(b=>/^\d+( ✓)?$/.test(b.label));assert.equal(cards.length,10);assert.equal(ui.api.snapshot().selection.chapter,chapter);
for(const b of cards){assert.ok(b.w>=44&&b.h>=44);assert.ok(b.x>=0&&b.x+b.w<=w&&b.y+b.h<h-34-106);}
const layout=chapterLayout(w,h,92,34);assert.ok(layout.h>=44);
if(chapter<20)ui.button('下一页');
}
ui.button('200');closeRules(ui);ui.button('选关');assert.equal(ui.api.snapshot().selection.chapter,20);assert.ok(ui.api.buttons().some(b=>b.label==='200'));
}
});
test('hint-only frames reuse static UI, and player input invalidates it',()=>{
const ui=boot();ui.select(3);ui.button('指路');ui.advance(1500);const before=ui.api.snapshot().hintBaseHits;
for(let i=0;i<12;i++)ui.advance(16);assert.ok(ui.api.snapshot().hintBaseHits>=before+10);
ui.cell(solve(levels[3])[0][0]);assert.equal(ui.api.snapshot().path.length,1);ui.button('撤销');assert.equal(ui.api.snapshot().path.length,0);
ui.button('规则');assert.ok(ui.api.buttons().some(b=>b.label==='返回游戏'));ui.button('返回游戏');assert.ok(ui.api.snapshot().hint);
});
test('hint curves are cached and distance lookup is stable at boundaries',()=>{
const hint={guides:[{a:'a',b:'b',points:[[0,0],[50,0],[50,50]]}]};const motion=prepareHintMotion(hint,40,[]);assert.equal(prepareHintMotion(hint,40,[]),motion);assert.notEqual(prepareHintMotion(hint,40,['a','b']),motion);
assert.equal(hintPointAt(motion,0).x,0);assert.equal(hintPointAt(motion,motion.total).y,50);
assert.equal(prepareHintMotion(hint,40,['a','b']).total,0);
});
test('new-mechanic lesson reuses the board while the continuous cursor advances',()=>{
const ui=boot();ui.select(4);ui.button('规则');const before=ui.api.snapshot();
for(let i=0;i<6;i++)ui.advance(1);
const after=ui.api.snapshot();assert.ok(after.renderedFrames>=before.renderedFrames+6);assert.equal(after.lessonBoardRenders,before.lessonBoardRenders);assert.deepEqual(after.path,before.path);
ui.button('返回游戏');ui.button('规则');assert.ok(ui.api.snapshot().lesson);
});