Files
NumberSwipe_New/tests/offline-ui.test.mjs
T

29 lines
5.8 KiB
JavaScript

import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import vm from 'node:vm';import {definitions} from '../tools/definitions.mjs';import {solve,graph} from '../tools/engine-template.mjs';
function boot(mode,width=340){const draws=[],elements=new Map();let context;
class Element{constructor(id=''){this.id=id;this.children=[];this.handlers={};this.hidden=false;this.disabled=false;this.value='';this.textContent='';this.className='';}replaceChildren(){this.children=[];}append(e){this.children.push(e);}addEventListener(n,f){this.handlers[n]=f;}setPointerCapture(){}getBoundingClientRect(){return {left:0,top:0,width,height:420};}click(){this.onclick?.();}}
const ctx={globalAlpha:1,lineWidth:1,font:'',fillStyle:'',strokeStyle:'',textAlign:'center',textBaseline:'middle',setTransform(){},clearRect(){draws.length=0;},beginPath(){this.p=[];},moveTo(x,y){this.p.push([x,y]);},lineTo(x,y){this.p.push([x,y]);},closePath(){},fill(){draws.push({kind:'fill',points:this.p,color:this.fillStyle});},stroke(){draws.push({kind:'stroke',points:this.p,color:this.strokeStyle});},fillText(s,x,y){draws.push({kind:'text',text:s,x,y,font:this.font});},save(){},restore(){},translate(){},rotate(){},setLineDash(){}};
function get(id){if(!elements.has(id))elements.set(id,new Element(id));return elements.get(id);}
const canvas=get('board');canvas.clientWidth=width;canvas.clientHeight=420;canvas.getContext=()=>ctx;
const document={getElementById:get,createElement:()=>new Element()},window={devicePixelRatio:1,addEventListener(){}};
context=vm.createContext({document,window,localStorage:{getItem:()=>null,setItem(){}},performance:{now:()=>100},setTimeout,URL,Blob,console});
const html=fs.readFileSync(new URL('../modes/'+mode+'/play.html',import.meta.url),'utf8');const script=html.match(/<script>([\s\S]*)<\/script>/)[1];new vm.Script(script).runInContext(context);
const snapshot=()=>JSON.parse(JSON.stringify(window.variantSnapshot()));
function press(id){const p=snapshot().centers[id];canvas.handlers.pointerdown({preventDefault(){},pointerId:1,clientX:p[0],clientY:p[1]});canvas.handlers.pointerup({});}
return {get,snapshot,press,canvas,draws,width};
}
for(const def of definitions)test('embedded page lifecycle and all 8 levels '+def.id,()=>{const ui=boot(def.id);const pack=JSON.parse(fs.readFileSync(new URL('../modes/'+def.id+'/levels.json',import.meta.url),'utf8'));
for(let i=0;i<8;i++){ui.get('difficulties').children[Math.floor(i/2)].click();ui.get('levels').children[i%2].click();const l=pack.levels[i],p=solve(l)[0];assert.equal(ui.snapshot().level,l.id);assert.equal(ui.get('levels').children.length,2);assert.equal(ui.get('difficulties').children.length,4);
const before=ui.snapshot();for(const center of Object.values(before.centers)){assert.ok(center[0]>=0&&center[0]<=ui.width);assert.ok(center[1]>=0&&center[1]<=420);}assert.ok(before.unit>=20);
ui.get('hint').click();assert.match(ui.get('status').textContent,/黄色/);
ui.press(p[0]);ui.press(p[1]);assert.deepEqual(ui.snapshot().path,p.slice(0,2));ui.press(p[0]);assert.deepEqual(ui.snapshot().path,p.slice(0,1));ui.get('undo').click();assert.deepEqual(ui.snapshot().path,[]);
for(const id of p)ui.press(id);assert.equal(ui.snapshot().cleared,true);assert.equal(ui.get('remaining').textContent,'完成 ✓');
ui.get('undo').click();assert.equal(ui.snapshot().cleared,false);assert.equal(ui.snapshot().path.length,p.length-1);ui.press(p.at(-1));assert.equal(ui.snapshot().cleared,true);
ui.get('restart').click();assert.deepEqual(ui.snapshot().path,[]);assert.equal(ui.snapshot().cleared,false);assert.ok(ui.snapshot().openedGates.every(v=>!v));
if(l.geometry==='blocks')assert.ok(l.cells.some(c=>c.w>1));
}
});
test('fast drag samples crossed cells and preserves prefix after release',()=>{const ui=boot('walls'),p=solve(JSON.parse(fs.readFileSync(new URL('../modes/walls/levels.json',import.meta.url),'utf8')).levels[0])[0];const start=ui.snapshot().centers[p[0]],next=ui.snapshot().centers[p[1]];ui.canvas.handlers.pointerdown({preventDefault(){},pointerId:1,clientX:start[0],clientY:start[1]});ui.canvas.handlers.pointermove({preventDefault(){},clientX:next[0],clientY:next[1]});ui.canvas.handlers.pointerup({});assert.deepEqual(ui.snapshot().path,p.slice(0,2));});
test('step door UI shows current and next state and undo/restart restore it',()=>{const ui=boot('rhythm-door'),l=JSON.parse(fs.readFileSync(new URL('../modes/rhythm-door/levels.json',import.meta.url),'utf8')).levels[0],p=solve(l)[0];assert.deepEqual(ui.snapshot().doors,[false]);assert.match(ui.get('constraints').children[0].textContent,/门关.*就开/);ui.press(p[0]);assert.deepEqual(ui.snapshot().doors,[true]);assert.match(ui.get('constraints').children[0].textContent,/门开.*就关/);ui.press(p[1]);assert.deepEqual(ui.snapshot().doors,[false]);ui.get('undo').click();assert.deepEqual(ui.snapshot().doors,[true]);ui.get('restart').click();assert.deepEqual(ui.snapshot().doors,[false]);});
test('alternation keeps blue/orange fill after visits and uses clear colour legend',()=>{for(const mode of ['block-alternation','hex-alternation']){const ui=boot(mode),l=JSON.parse(fs.readFileSync(new URL('../modes/'+mode+'/levels.json',import.meta.url),'utf8')).levels[0],p=solve(l)[0];const colours=()=>ui.draws.filter(d=>d.kind==='fill').map(d=>d.color);assert.ok(colours().includes('#64acf3'));assert.ok(colours().includes('#ffae55'));ui.press(p[0]);ui.press(p[1]);assert.ok(colours().includes('#64acf3'));assert.ok(colours().includes('#ffae55'));assert.match(ui.get('constraints').children[0].textContent,/蓝.*橙.*换颜色/);}});
test('retired modes are absent from the active launcher',()=>{const html=fs.readFileSync(new URL('../index.html',import.meta.url),'utf8');for(const id of ['relative','order','window','gates'])assert.ok(!html.includes('modes/'+id+'/'));assert.ok(html.includes('modes/walls/'));assert.ok(html.includes('modes/rhythm-door/'));});