Files
shanhe-puzzle/tests/frame-loop.test.cjs
codex-bot 4845826deb
Shanhe source and offline package checks / verify (pull_request) Failing after 5s
feat: sync Shanhe 2.2 source and add remote package verification
2026-11-01 17:13:46 +08:00

10 lines
3.8 KiB
JavaScript

const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'),{parse}=require('acorn');
const source=fs.readFileSync('src/app.js','utf8'),names=['loopNow','writeLoopHealth','recordLoopError','captureVideoFrame','pauseAfterFrameError','schedule','frame'];
const functions=parse(source,{ecmaVersion:2017}).body[0].expression.callee.body.body.filter(n=>n.type==='FunctionDeclaration'&&names.includes(n.id.name)).map(n=>source.slice(n.start,n.end)).join('\n');
function scene(){const attrs={},nodes={},events={scheduled:0,modal:0,toast:0,disposed:0,logs:[],ticks:0};let now=0;const box={window:{performance:{now:()=>++now},console:{}},console:{error:(...args)=>events.logs.push(args)},document:{hidden:false},canvas:{setAttribute:(k,v)=>attrs[k]=v},loopHealth:{build:'test',frames:0,stage:'idle',physicsMs:0,drawMs:0,videoMs:0,error:null},healthAt:-Infinity,game:{status:'playing',endReason:null,level:{id:'510100'},groups:[],dropped:19,uprightCount:1,shake:{offset:0,y:0},wallHitUntil:0,clock:0,tick(){events.ticks++;}},level:{id:'510100'},screen:'game',last:0,rafId:9,uiAt:0,videoCapture:null,videoNotice:'',pausedByModal:false,victoryAge:0,Engine:{VICTORY_MS:2600},requestAnimationFrame(){events.scheduled++;return events.scheduled;},$:id=>nodes[id]||(nodes[id]={style:{},classList:{toggle(){}}}),draw(){},videoMeta(){return {};},updateUI(){},showResult(){box.screen='result';},stopInput(){},openModal(){events.modal++;},finishJourney(){},closeModal(){},toast(){events.toast++;}};vm.createContext(box);vm.runInContext(functions,box);return {box,events,attrs};}
test('正常帧持续调度,DOM诊断记录正在推进的帧数和当前关卡',()=>{const {box,events,attrs}=scene();box.frame(100);box.frame(117);assert.equal(events.scheduled,2);assert.equal(events.ticks,2);const report=JSON.parse(attrs['data-loop-health']);assert.equal(report.level,'510100');assert.ok(report.frames>0);assert.equal(report.error,null);});
test('物理帧异常明确暂停并保留对局,不能无声丢失下一帧',()=>{const {box,events,attrs}=scene(),g=box.game;box.game.tick=()=>{throw Error('fixture physics failure');};box.frame(100);assert.equal(box.game,g);assert.equal(g.status,'paused');assert.equal(events.modal,1);assert.equal(events.scheduled,0);const report=JSON.parse(attrs['data-loop-health']);assert.equal(report.error.stage,'physics');assert.match(report.error.message,/fixture/);assert.equal(report.dropped,19);g.tick=()=>{events.ticks++;};g.status='playing';box.frame(117);assert.equal(events.ticks,1);assert.equal(events.scheduled,1);});
test('绘制异常记录具体阶段并保留可继续的暂停界面',()=>{const {box,events,attrs}=scene();box.draw=()=>{throw Error('fixture draw failure');};box.frame(100);assert.equal(events.ticks,1);assert.equal(events.modal,1);assert.equal(JSON.parse(attrs['data-loop-health']).error.stage,'draw');assert.equal(box.game.status,'paused');});
test('视频采样异常只关闭视频记录,主游戏与后续动画继续',()=>{const {box,events,attrs}=scene();box.videoCapture={capture(){throw Error('fixture video failure');},dispose(){events.disposed++;}};box.frame(100);assert.equal(box.game.status,'playing');assert.equal(box.videoCapture,null);assert.equal(events.modal,0);assert.equal(events.disposed,1);assert.equal(events.scheduled,1);assert.equal(JSON.parse(attrs['data-loop-health']).error.stage,'video');box.frame(117);assert.equal(events.ticks,2);assert.equal(events.scheduled,2);assert.equal(events.logs.length,1);});
test('界面更新异常也记录阶段;后台页面不再调度动画',()=>{const {box,events,attrs}=scene();box.updateUI=()=>{throw Error('fixture ui failure');};box.frame(200);assert.equal(box.game.status,'paused');assert.equal(JSON.parse(attrs['data-loop-health']).error.stage,'ui');box.game.status='playing';box.document.hidden=true;box.rafId=0;box.schedule();assert.equal(events.scheduled,0);});