Files

56 lines
4.4 KiB
JavaScript

'use strict';
const {test}=require('node:test');const assert=require('node:assert/strict');const fs=require('node:fs');const path=require('node:path');const vm=require('node:vm');
test('终止后较早的 heartbeat 响应不能恢复流程',async()=>{
const handlers=new Map(),requests=[],responses=[],values={pairToken:'test-token',pairPort:8770};
class Event{constructor(type,options={}){this.type=type;this.detail=options.detail;}}
const doc={readyState:'loading',addEventListener(type,fn){handlers.set(type,fn);},dispatchEvent(event){if(event.type==='dww11:local-response')responses.push(JSON.parse(event.detail));else handlers.get(event.type)?.(event);}};
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../browser/local-bridge.user.js'),'utf8'),{document:doc,CustomEvent:Event,GM_getValue:(k,d)=>values[k]??d,GM_setValue:(k,v)=>values[k]=v,GM_addValueChangeListener:()=>{},GM_xmlhttpRequest:r=>requests.push(r)});
const call=(id,route)=>handlers.get('dww11:local-request')(new Event('',{detail:JSON.stringify({id,route})}));
const heartbeat=call('old','heartbeat');const stop=call('stop','stop');
requests[0].onload({status:200,responseText:JSON.stringify({running:true,epoch:1})});await heartbeat;
assert.equal(responses.find(r=>r.id==='old').value.running,false);assert.equal(values.halted,true);
requests[1].onload({status:200,responseText:JSON.stringify({running:false,epoch:2})});await stop;
});
test('悬浮窗开始、暂停、跳过与开始响应竞态',async()=>{
const handlers=new Map(),elements=new Map(),emitted=[],requests=[];
const element=()=>({style:{},before(child){elements.set(child.id,child);}});
for(const id of ['halt','port','token','save','status'])elements.set(id,element());
const doc={readyState:'complete',body:{append(){}},getElementById:()=>null,
createElement:()=>({...element(),attachShadow:()=>({set innerHTML(value){},getElementById:id=>elements.get(id)})}),
addEventListener(type,fn){if(!handlers.has(type))handlers.set(type,[]);handlers.get(type).push(fn);},
dispatchEvent(event){emitted.push(event);for(const fn of handlers.get(event.type)||[])fn(event);}
};
class Event{constructor(type,options={}){this.type=type;this.detail=options.detail;}}
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../browser/local-bridge.user.js'),'utf8'),{
document:doc,CustomEvent:Event,GM_getValue:(k,d)=>k==='pairToken'?'test-token':k==='halted'?false:d,
GM_setValue:()=>{},GM_addValueChangeListener:()=>{},GM_xmlhttpRequest:r=>requests.push(r)
});
const skip=elements.get('skip');assert.equal(skip.textContent,'跳过本文章');skip.onclick();
assert.equal(emitted.at(-1).type,'dww11:skip-article');
doc.dispatchEvent(new Event('dww11:skip-busy',{detail:true}));assert.equal(skip.disabled,true);
doc.dispatchEvent(new Event('dww11:skip-busy',{detail:false}));assert.equal(skip.disabled,false);
doc.dispatchEvent(new Event('dww11:local-request',{detail:JSON.stringify({id:'skip-test',route:'skip',body:{id:'job'}})}));
assert.equal(requests[0].url,'http://127.0.0.1:8770/bridge/skip');
requests[0].onload({status:200,responseText:'{"ok":true}'});
const start=elements.get('start'),pause=elements.get('pause');
assert.equal(start.textContent,'开始 / 继续');assert.equal(pause.textContent,'暂停');
let started=start.onclick();assert.equal(start.disabled,true);
assert.ok(requests.at(-1).url.endsWith('/bridge/start'));
requests.at(-1).onload({status:200,responseText:'{"running":true}'});await started;
doc.dispatchEvent(new Event('dww11:run-state',{detail:true}));assert.equal(start.disabled,true);
const paused=pause.onclick();assert.equal(emitted.at(-1).type,'dww11:auto-stop');
assert.ok(requests.at(-1).url.endsWith('/bridge/pause'));
requests.at(-1).onload({status:200,responseText:'{"running":false}'});await paused;
doc.dispatchEvent(new Event('dww11:run-state',{detail:false}));assert.equal(start.disabled,false);
// Pause while Start is still in flight: a late Start must be followed by Pause.
started=start.onclick();const lateStart=requests.at(-1);
const pausing=pause.onclick();requests.at(-1).onload({status:200,responseText:'{"running":false}'});await pausing;
lateStart.onload({status:200,responseText:'{"running":true}'});
await new Promise(resolve=>setImmediate(resolve));
assert.ok(requests.at(-1).url.endsWith('/bridge/pause'));
requests.at(-1).onload({status:200,responseText:'{"running":false}'});await started;
assert.match(elements.get('status').textContent,/已暂停/);
});