bend_puzzle/test/frame-drag-scheduler-test.js
2026-07-31 12:54:46 +09:00

56 lines
3.2 KiB
JavaScript

'use strict';
const {assert}=require('./helpers/app-source');
const {createFrameScheduler}=require('../client/input/frame-scheduler');
const {createDragScheduler,STATES}=require('../client/input/drag');
function fakeClock(){
let now=0,nextId=1;
const frames=new Map(),timers=new Map();
return{
now:()=>now,
requestFrame:callback=>{const id=nextId++;frames.set(id,callback);return id},
cancelFrame:id=>frames.delete(id),
setDelay:(callback,delay)=>{const id=nextId++;timers.set(id,{callback,at:now+delay});return id},
clearDelay:id=>timers.delete(id),
advance(milliseconds){
now+=milliseconds;
for(const[id,entry]of[...timers])if(entry.at<=now){timers.delete(id);entry.callback()}
const pending=[...frames];frames.clear();for(const[,callback]of pending)callback(now);
}
};
}
const clock=fakeClock(),commits=[];
const scheduler=createFrameScheduler({
requestFrame:clock.requestFrame,cancelFrame:clock.cancelFrame,setDelay:clock.setDelay,clearDelay:clock.clearDelay,
now:clock.now,interval:1000/60,tolerance:1.25,watchdogDelay:18,
commit:(value,timestamp)=>commits.push({value,timestamp})
});
for(let index=0;index<40;index++){scheduler.push(index);clock.advance(2)}
clock.advance(20);
assert(commits.length<=6,'High-rate input produced more than one presentation commit per nominal 60 Hz interval');
assert(commits.at(-1).value===39,'Latest-value frame scheduling lost the newest input');
for(let index=1;index<commits.length;index++)assert(commits[index].timestamp-commits[index-1].timestamp>=15,'Presentation commits exceeded the 60 Hz tolerance');
const dragClock=fakeClock(),states=[];
const drag=createDragScheduler({
requestFrame:dragClock.requestFrame,cancelFrame:dragClock.cancelFrame,setDelay:dragClock.setDelay,clearDelay:dragClock.clearDelay,
now:dragClock.now,interval:1000/60,tolerance:1.25,watchdogDelay:18,maxSamples:4,
trim:samples=>{while(samples.length>4)samples.splice(1,1)},
onFrame:session=>states.push(session.inspect().state)
});
drag.arm(7);
assert(drag.inspect().state==='armed','Drag did not enter armed state');
for(let index=0;index<8;index++)drag.push({pointerId:7,clientX:index,clientY:index});
assert(drag.inspect().state==='running'&&drag.inspect().logicalCount===4&&drag.latest().clientX===7,'Drag scheduler did not separate bounded logical samples from latest visual input');
dragClock.advance(17);
assert(states[0]==='running','Drag scheduler did not commit through its running frame lane');
assert(drag.beginDrain({pointerId:7,clientX:8,clientY:8})&&drag.inspect().state==='draining','Drag did not enter draining state');
assert(drag.drainLogical().at(-1).clientX===8,'Release drain lost final input intent');
drag.beginSettling();assert(drag.inspect().state==='settling','Drag did not enter settling state');
drag.settle();assert(drag.inspect().state==='idle','Settled drag did not return to idle');
drag.arm(9);drag.cancel();assert(drag.inspect().state==='cancelled','Cancelled drag did not expose its terminal state');
assert(STATES.join(',')==='idle,armed,running,draining,settling,cancelled','Drag lifecycle states changed unexpectedly');
console.log('Shared 60 Hz frame scheduler and explicit drag lifecycle behavior passed');