91 lines
7.0 KiB
JavaScript
91 lines
7.0 KiB
JavaScript
const fs=require('node:fs'), vm=require('node:vm'), path=require('node:path'), assert=require('node:assert/strict');
|
|
const source=fs.readFileSync(path.join(__dirname,'../ui_builder/static/app.js'),'utf8');
|
|
const names=['currentHockeyPenaltyEntries','activeHockeyPenaltyEntries','hockeyPenaltyHasStarted',
|
|
'sortedPenaltyEntries','penaltyLocalAdvantageSide','penaltyDisplayEntriesByTargetSide',
|
|
'hockeyTimerSyncStepById','activateConfiguredPenaltyCountdowns','penaltyTargetAssignmentKey',
|
|
'penaltyMirrorKey','vmixCountdownValue','vmixGameCountdownControlCommands','vmixPenaltyCountdownControlCommands',
|
|
'rebalanceVmixPenaltyTargets','emitHockeyEventUpdated','hockeyEventReady','hockeyScoreboardIsLive',
|
|
'shortcutSequenceIsOnAir','sequenceMatchesRuntimeOverlay','runtimeOverlayMatchesInput',
|
|
'normalizeRuntimeVmixInputRef','runtimeOverlayInputRefs'];
|
|
const definitions=names.map(name=>{const start=source.search(new RegExp(' (?:async )?function '+name+'\\('));
|
|
assert.ok(start>=0,name);return source.slice(start,source.indexOf('\n }',start)+4);}).join('\n');
|
|
const target={id:'home-slot',input:'home-penalty',selected_name:'Time.Text',overlay:'2'};
|
|
const step={id:'timers',type:'hockey_vmix_timers_start',sync_vmix_penalties:true,home_penalty_targets:[target],away_penalty_targets:[]};
|
|
const scoreboard={id:'score',is_scoreboard_sequence:true};
|
|
const component={type:'hockey_penalty_dashboard',action_id:'penalties'};
|
|
const event={id:'p1',side:'away',player:{side:'away'},infraction:null,preset:'2',durationMs:120000,
|
|
remainingMs:120000,startedOnce:false,running:false,finished:false};
|
|
const board={penalties:[event]};
|
|
const sent=[];let allowed=true;
|
|
const state={config:{components:[component],shortcut_sequences:[scoreboard,{id:'space',steps:[step]}]},
|
|
data:{},hockeyPenaltyAdvantageCycle:{},activeHockeyVmixTimerSteps:new Set(),
|
|
vmixPenaltyTargetAssignments:new Map(),vmixPenaltyMirrors:new Map(),vmixOverlayTallyAuthoritative:true,
|
|
vmixOverlayRuntime:new Map([['1',{input:'score-input'}]]),shortcutSequenceOverlayState:new Map(),
|
|
quickPanelServerOnAirSequences:new Set(),quickPanelOnAirSequences:new Set()};
|
|
const noop=()=>{};
|
|
const ctx={console,state,performance,Date,ensureHockeyBoardState:()=>board,hockeyCanControl:()=>allowed,
|
|
padTimer:n=>String(n).padStart(2,'0'),sequencePenaltyTargets:(s,side)=>s[side+'_penalty_targets'],
|
|
getByPath:()=>({}),rememberPenaltyAdvantagePlan:noop,emitInteraction:noop,hockeyPenaltyContext:()=>({}),
|
|
addHockeyHistory:noop,hockeyInfractionWebLabel:()=>'',hockeySyncPenaltySideMappingContext:()=>Promise.resolve(),
|
|
sendRuntimeVmixTimerSequence:commands=>{sent.push(...commands);return Promise.resolve({ok:true});},
|
|
shortcutSequenceById:id=>state.config.shortcut_sequences.find(s=>s.id===id),
|
|
shortcutSequenceOverlayTargets:s=>s?.id==='score'?[{layer:'1',input:'score-input'}]:[],
|
|
shortcutSequencePreviewTargets:()=>[],sequenceStillOwnsRuntimeOverlay:()=>false};
|
|
vm.createContext(ctx);vm.runInContext(definitions,ctx);
|
|
const functions=()=>sent.map(c=>c.Function);
|
|
(async()=>{
|
|
// No local ON AIR latch, only authoritative vMix tally (e.g. after reload).
|
|
assert.equal(ctx.hockeyScoreboardIsLive(),true);
|
|
ctx.emitHockeyEventUpdated(component,event,'player');
|
|
assert.deepEqual(functions(),[],'an incomplete draft must not display');
|
|
event.infraction={id:'trip'};
|
|
ctx.emitHockeyEventUpdated(component,event,'infraction');
|
|
assert.deepEqual(functions(),['SetCountdown','StopCountdown','OverlayInput2In']);
|
|
assert.equal(sent[0].Value,'00:02:00');
|
|
assert.ok(sent.every(c=>c.Input==='home-penalty'));
|
|
assert.equal(event.running,false);assert.equal(event.startedOnce,false);
|
|
sent.length=0;ctx.emitHockeyEventUpdated(component,event,'player');
|
|
assert.deepEqual(functions(),[],'duplicate ready notification must not reseed or redisplay');
|
|
event.durationMs=event.remainingMs=240000;
|
|
ctx.emitHockeyEventUpdated(component,event,'preset');
|
|
assert.deepEqual(functions(),['SetCountdown','StopCountdown']);assert.equal(sent[0].Value,'00:04:00');
|
|
// Before F1, off-air preparation sends nothing. Showing the scorebug displays
|
|
// the penalty even if its target was assigned previously but hidden later.
|
|
sent.length=0;state.vmixOverlayRuntime.clear();
|
|
state.shortcutSequenceOverlayState.set('score',true);
|
|
assert.equal(ctx.hockeyScoreboardIsLive(),false,'authoritative OUT wins over stale local latch');
|
|
event.durationMs=event.remainingMs=300000;
|
|
ctx.emitHockeyEventUpdated(component,event,'preset');
|
|
assert.deepEqual(functions(),['SetCountdown','StopCountdown'],'editing a previously assigned hidden Input updates its stopped time');
|
|
assert.equal(sent[0].Value,'00:05:00');
|
|
sent.length=0;state.vmixPenaltyTargetAssignments.clear();state.activeHockeyVmixTimerSteps.clear();
|
|
ctx.emitHockeyEventUpdated(component,event,'preset');assert.deepEqual(functions(),[],'cold off-air draft sends nothing');
|
|
ctx.activateConfiguredPenaltyCountdowns();
|
|
await ctx.rebalanceVmixPenaltyTargets({scoreboardActive:true,forceOverlay:true});
|
|
assert.deepEqual(functions(),['SetCountdown','StopCountdown','OverlayInput2In']);
|
|
assert.equal(sent[0].Value,'00:05:00');
|
|
sent.length=0;await ctx.rebalanceVmixPenaltyTargets({scoreboardActive:true,forceOverlay:true});
|
|
assert.deepEqual(functions(),['OverlayInput2In'],'showing scorebug again must not restart native timer');
|
|
sent.length=0;allowed=false;state.vmixOverlayRuntime.set('1',{input:'score-input'});
|
|
ctx.emitHockeyEventUpdated(component,event,'preset');await ctx.rebalanceVmixPenaltyTargets({force:true});
|
|
assert.deepEqual(functions(),[],'viewer never sends a title or timer');
|
|
// A failed dispatch must not leave an Input marked as freshly seeded.
|
|
allowed=true;
|
|
const start=source.indexOf(' function sendRuntimeVmixTimerSequence(');
|
|
vm.runInContext(source.slice(start,source.indexOf('\n }',start)+4),ctx);
|
|
Object.assign(ctx,{compactVmixCommand:c=>c,currentRuntimeVmixDeviceId:()=> 'd',currentRuntimeHockeySessionToken:()=> 's',
|
|
trackRuntimeOverlayCommands:noop,applyServerRuntimeOverlayState:noop,console:{warn:noop}});
|
|
ctx.fetch=async()=>({ok:false,status:503,json:async()=>({})});
|
|
await ctx.sendRuntimeVmixTimerSequence([{Function:'SetCountdown',Input:'home-penalty',SelectedName:'Time.Text',Value:'00:05:00'}]);
|
|
assert.equal(state.vmixPenaltyTargetAssignments.size,0,'failed send clears the cached assignment for a later fresh seed');
|
|
const key='timers:home:home-slot';
|
|
state.vmixPenaltyTargetAssignments.set(key,{input:'home-penalty'});
|
|
let finish;
|
|
ctx.fetch=()=>new Promise(resolve=>{finish=resolve});
|
|
const oldRequest=ctx.sendRuntimeVmixTimerSequence([{Function:'StartCountdown',Input:'home-penalty'}]);
|
|
const newer={input:'home-penalty',running:true};state.vmixPenaltyTargetAssignments.set(key,newer);
|
|
finish({ok:false,status:503,json:async()=>({})});await oldRequest;
|
|
assert.equal(state.vmixPenaltyTargetAssignments.get(key),newer,'late failed response must keep newer assignment');
|
|
console.log('PASS: prepared penalty display, fresh stopped time, Agent tally, off-air, F1 redisplay, viewer');
|
|
})().catch(e=>{console.error(e);process.exitCode=1;});
|