Files
saphid--frame-control/tests/control_center.cjs

472 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Real Control Center logic with a minimal DOM; no network, browser or headset.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const cc = require('../ui/control-center.js');
const sample = () => ({time:Date.now()/1000, hostname:'Test Frame', battery:{percent:99,status:'Discharging',watts:0,tempC:33,health:'Good'},
power:{type:'C PD [PD_PPS]',watts:18}, disk:{home:{total:256*1073741824,free:128*1073741824}},
memory:{total:16*1073741824,available:6*1073741824}, wifi:{ssid:'Test LAN',signal:80},
performance:{compositorFps:90,appFps:45,frameMs:11.11,gpuMs:3,cpuPercent:12,gpuMHz:600},
volume:{level:.4,muted:false}, os:{version:'0.4.1',build:'test-build'},services:{steamvr:true}, thermalAlerts:[],games:[],flatpaks:[]});
function fixture() {
const elements = new Map();
function element(id) {
if (!elements.has(id)) elements.set(id, {id,hidden:false,disabled:false,textContent:'',innerHTML:'',value:'',dataset:{},
classList:{toggle(){}},querySelectorAll(){return [];},setAttribute(key,value){this[key]=value;},addEventListener(key,callback){this[key]=callback;},focus(){this.focused=true;}});
return elements.get(id);
}
const cards = cc.catalog.map(item=>{const e=element(item.id);e.dataset.ccCard=item.id;return e;});
const buttons = cc.categories.map(([id])=>{const e=element('category-'+id);e.dataset.ccCategory=id;e.querySelector=()=>element('count-'+id);return e;});
const metrics = Object.keys(cc.metrics(null)).map(id=>{const e=element('metric-'+id);e.dataset.ccMetric=id;return e;});
const page = element('controlCenter');
page.querySelectorAll = selector => selector === '[data-cc-category]' ? buttons : selector === '[data-cc-metric]' ? metrics : selector === '[data-cc-mac-link]' ? [element('macLink')] : [];
page.querySelector = selector => selector === '[href="#connection"]' ? element('connectionLink') : cards.find(e=>selector === `[data-cc-card="${e.dataset.ccCard}"]`);
return {document:{getElementById:element},element,cards,buttons};
}
function mounted(options = {}) {
const f = fixture(), calls = [];
let context = {online:true,generation:1,device:'frame-a'}, refreshes = 0, opened = 0;
const ui = cc.mount({document:f.document,platform:'darwin',api:async(...args)=>{calls.push(args);return {message:'Volume updated'};},
refresh:async()=>{refreshes++;},getContext:()=>context,openConnection:()=>opened++,...options});
return {...f,ui,calls,get context(){return context;},set context(v){context=v;},get refreshes(){return refreshes;},get opened(){return opened;}};
}
const pageSource = fs.readFileSync(require.resolve('../ui/index.html'),'utf8');
const deferred = () => { let resolve, reject; const promise=new Promise((ok,bad)=>{resolve=ok;reject=bad;});return {promise,resolve,reject}; };
const flush = async () => { for(let i=0;i<12;i++)await Promise.resolve(); };
// Execute the actual shared refresh/deduplication, host callback and mount
// bootstrap, including the real Home-to-Control status handoff. Only unrelated
// rendering helpers, timers and transport are stubbed.
function sharedPage({productionApi=false} = {}) {
const f=fixture(), requests=[], writes=[], host=deferred(), job=deferred();
const context={document:f.document,FrameControlCenter:cc,window:{frameApp:{setUpConnection(){}}},
Date,setInterval,clearInterval,setTimeout:()=>1,clearTimeout(){},$:f.element,QUICK:[],installing:new Set(),
battery(){},setVolume(){},gb:String,dur:String,esc:String,meter(){},toast(){},
runJob(title,id){context.installing.add(id);return job.promise;},
api:(path,body,device)=>{
if(path==='/api/host')return host.promise;
if(path==='/api/status'){const request=deferred();requests.push(request);return request.promise;}
writes.push({path,body,device});return Promise.resolve({message:'Volume updated'});
},renderPerformance(){},setConn(){},setOnline(ok){vm.runInContext(`online = ${ok}`,context);},log(){},failed(){},
watchConnection(){context.watched=true;}};
vm.createContext(context);
const stateDeclaration=pageSource.match(/let state = null[^\n]*/)[0];
vm.runInContext(stateDeclaration+'\nlet online = true, devGen = 1; const HOST = {computer:"computer"}; window.connDevice="frame-a";',context);
const refreshStart=pageSource.indexOf('let refreshing = null'),refreshEnd=pageSource.indexOf('// ---- our VR HUD',refreshStart);
const renderStart=pageSource.indexOf('function render(s) {'),renderEnd=pageSource.indexOf('// ---- view ----',renderStart);
vm.runInContext(pageSource.slice(renderStart,renderEnd)+pageSource.slice(refreshStart,refreshEnd),context);
if(productionApi) {
// Keep api/onConnection/setOnline intact: defer fetch itself, not api(), so
// rejection classification and the real switch/reset notifications run.
context.fetch=(path,opts)=>{
if(path==='/api/host')return host.promise.then(data=>({ok:true,status:200,json:async()=>data}));
const request=deferred();request.path=path;request.opts=opts;requests.push(request);return request.promise;
};
Object.assign(context,{renderPill(){},renderConnDlg(){},renderBanner(){},viaText(){},netLabel(){},
resetShots(){},loadDevices(){},renderDevices(){},renderTests(){},reloadAll(){},checkContactPrompt(){}});
vm.runInContext(`const UI_KEY = 'test-only';
const link = {s:null,device:'frame-a',phase:'connected',rev:0,net:null};
const disp = {}, gm = {seq:0}, cat = {}, wi = {};
let viewGen = 0, lastImg = null, lastShot = null, androidApps = [], titlesSeq = 0, page = 'control';`,context);
const apiStart=pageSource.indexOf('const HEADSET_FREE ='),apiEnd=pageSource.indexOf('// What a panel shows',apiStart);
const onlineStart=pageSource.indexOf('function setOnline('),onlineEnd=pageSource.indexOf('function reloadAll()',onlineStart);
const connectionStart=pageSource.indexOf('function onConnection('),connectionEnd=pageSource.indexOf('// Read the event stream',connectionStart);
vm.runInContext(pageSource.slice(apiStart,apiEnd)+pageSource.slice(onlineStart,onlineEnd)+pageSource.slice(connectionStart,connectionEnd),context);
}
const installStart=pageSource.indexOf('function installFlatpak(id) {'),installEnd=pageSource.indexOf('$("fpInstall").onclick',installStart);
vm.runInContext(pageSource.slice(installStart,installEnd),context);
const hostStart=pageSource.lastIndexOf('api("/api/host").then(h =>'),hostEnd=pageSource.indexOf('</script>',hostStart);
vm.runInContext(pageSource.slice(hostStart,hostEnd),context);
const bootstrapStart=pageSource.indexOf('window.frameControlCenter = FrameControlCenter.mount('),bootstrapEnd=pageSource.indexOf('</script>',bootstrapStart);
return {...f,context,requests,writes,host,refresh:()=>vm.runInContext('refresh()',context),
switchHeadset:()=>vm.runInContext('onConnection({now:Date.now()/1000,phase:"connected",devices_rev:0,device:{id:"frame-b",name:"Second Frame"}})',context),
installFlatpak:()=>vm.runInContext('installFlatpak("test.app")',context),
mount(){vm.runInContext(pageSource.slice(bootstrapStart,bootstrapEnd),context);return context.window.frameControlCenter;}};
}
const response = data => ({ok:true,status:200,json:async()=>data});
for(const rejection of ['transport','body'])for(const delayedMount of [false,true]) {
test(`production ${rejection} rejection from A preserves B ${delayedMount ? 'before delayed mount' : 'after mount'}`, async t => {
const f=sharedPage({productionApi:true});
if(!delayedMount){const ui=f.mount();t.after(()=>ui.destroy());}
const old=f.refresh(),body=deferred();
assert.equal(f.requests[0].path,'/api/status');
if(rejection==='body') {
f.requests[0].resolve({ok:true,status:200,json:()=>body.promise});await flush();
}
f.switchHeadset();
assert.equal(f.requests.length,2,'real connected B callback starts its own status fetch');
const next=sample();next.hostname='Second Frame';next.battery.percent=20;next.volume.level=.2;
f.requests[1].resolve(response(next));await flush();
const receipt=vm.runInContext('stateReceivedAt',f.context);
assert.equal(vm.runInContext('stateRead.valid && online && devGen === 2',f.context),true);
if(!delayedMount)assert.equal(f.element('metric-battery').textContent,'20%');
(rejection==='body' ? body : f.requests[0]).reject(Error('Old A request failed'));await old;
assert.equal(vm.runInContext('stateRead.valid',f.context),true,'A must not invalidate B');
assert.equal(vm.runInContext('stateRead.error',f.context),null);
assert.equal(vm.runInContext('stateReceivedAt',f.context),receipt);
if(delayedMount){const ui=f.mount();t.after(()=>ui.destroy());}
const assertB=()=>{
assert.equal(f.element('ccDeviceName').textContent,'Second Frame');
assert.equal(f.element('metric-battery').textContent,'20%');
assert.equal(f.element('ccVolume').value,20);
for(const id of ['ccVolume','ccApplyVolume','ccMute'])assert.equal(f.element(id).disabled,false);
assert.doesNotMatch(f.element('ccSampleState').textContent,/No live sample|local server|Old A/);
};
assertB();vm.runInContext('render(state)',f.context);assertB();
assert.equal(f.requests.length,2,'preservation and redraw need no recovery poll');
// An ordinary failure on B must still invalidate the current sample.
const failed=f.refresh();
if(rejection==='body')f.requests[2].resolve({ok:true,status:200,json:async()=>{throw Error('B body failed');}});
else f.requests[2].reject(Error('B transport failed'));
await failed;
assert.equal(vm.runInContext('stateRead.valid',f.context),false);
assert.equal(f.element('metric-battery').textContent,'Unavailable');
for(const id of ['ccVolume','ccApplyVolume','ccMute'])assert.equal(f.element(id).disabled,true);
assert.equal(f.element('ccStatusLabel').textContent,'No live sample');
});
}
test('production headset-free API keeps transport errors and responses across a switch', async t => {
for(const failure of [false,true]) {
const f=sharedPage({productionApi:true}),ui=f.mount();t.after(()=>ui.destroy());
const pending=vm.runInContext('api("/api/devices")',f.context);
const result=pending.then(value=>({value}),error=>({error}));
f.switchHeadset();f.requests[1].resolve(response(sample()));await flush();
if(failure)f.requests[0].reject(Error('Device list transport failed'));
else f.requests[0].resolve(response({devices:['frame-b']}));
const settled=await result;
if(failure){assert.equal(settled.error.stale,undefined);assert.match(settled.error.message,/local server isn't running/);}
else assert.deepEqual(settled.value,{devices:['frame-b']});
assert.equal(vm.runInContext('stateRead.valid',f.context),true);
}
});
test('search finds settings by aliases, all words, category and guidance', () => {
const find = (q,category='all')=>cc.catalog.filter(item=>cc.matches(item,q,category)).map(item=>item.id);
assert.ok(find('DPI').includes('display'));
assert.ok(find('sleep inactivity').includes('power'));
assert.ok(find('MOTION SICKNESS').includes('playspace'));
assert.ok(find('font','apps').includes('display'));
assert.deepEqual(find('font','vr'),[]);
assert.deepEqual(find('<script>alert(1)</script>'),[]);
assert.ok(find(' ').length > 20);
});
test('metrics use real fields, preserve zero, and distinguish unavailable values', () => {
const m = cc.metrics(sample());
assert.equal(m.battery,'99%');
assert.equal(m.power,'0 W');
assert.match(m.batteryDetail,/not charging/);
assert.equal(m.fps,'90'); assert.equal(m.appFps,'45');
assert.equal(m.storage,'128 GiB'); assert.equal(m.memory,'10 GiB');
assert.equal(m.thermal,'No hot / critical trip reported');
assert.equal(cc.metrics(null).fps,'Unavailable');
assert.equal(cc.metrics({performance:{appFps:null,gpuMHz:0,cpuPercent:0}}).appFps,'Unavailable');
assert.equal(cc.metrics({performance:{cpuPercent:0}}).cpu,'0%');
assert.equal(cc.metrics({battery:{percent:0,status:'Discharging'}}).battery,'0%');
assert.match(cc.metrics({thermalAlerts:[{zone:'battery',tempC:45,limitC:44}]}).thermal,/battery 45 °C ≥ 44 °C/);
});
test('audio controller validates before any API call and rejects offline or expired readings', async () => {
let context={online:true,fresh:true,sample:sample(),device:'frame-a',generation:1},calls=[];
const controller=cc.createController({api:async(...args)=>{calls.push(args);return {};},getContext:()=>context});
for (const change of [{level:-.1},{level:1.1},{level:NaN},{level:Infinity},{level:'0.5'},{level:null},{muted:'false'},{unknown:true},{}]) {
await assert.rejects(controller.audio(change),/valid mute state/);
}
context.online=false;await assert.rejects(controller.audio({level:.5}),/Connect and refresh/);
context.online=true;context.fresh=false;await assert.rejects(controller.audio({muted:true}),/Connect and refresh/);
assert.equal(calls.length,0);
});
test('audio uses the captured device, prevents duplicate writes, and ignores switched-headset success', async () => {
let context={online:true,fresh:true,sample:sample(),device:'frame-a',generation:1},complete,calls=[];
const controller=cc.createController({api:(...args)=>{calls.push(args);return new Promise(r=>complete=r);},getContext:()=>context});
const pending=controller.audio({level:.7});
await assert.rejects(controller.audio({muted:true}),/still in progress/);
assert.deepEqual(calls,[['/api/volume',{level:.7},'frame-a']]);
context={...context,device:'frame-b',generation:2};complete({message:'Done'});
assert.equal(await pending,null);assert.equal(controller.busy,false);
});
test('controller surfaces real API errors and allows a deliberate retry', async () => {
let calls=0;
const controller=cc.createController({api:async()=>{if(++calls===1)throw Error('SSH connection lost');return {message:'Done'};},
getContext:()=>({online:true,fresh:true,sample:sample(),device:'frame-a',generation:1})});
await assert.rejects(controller.audio({muted:true}),/SSH connection lost/);
assert.equal(controller.busy,false);
assert.deepEqual(await controller.audio({muted:true}),{message:'Done'});
});
test('mounted controls only write on Apply or Mute, show readback, and reset on switch', async t => {
const f=mounted();t.after(()=>f.ui.destroy());
assert.equal(f.element('ccApplyVolume').disabled,true);
f.ui.status(sample());
assert.equal(f.element('ccApplyVolume').disabled,false);
assert.equal(f.element('ccVolume').value,40);
f.element('ccVolume').value='65';f.element('ccVolume').oninput();
assert.equal(f.calls.length,0);
assert.equal(f.element('ccVolumeValue').textContent,'65%');
f.ui.status(sample()); // a poll must not overwrite an unapplied value
assert.equal(f.element('ccVolume').value,'65');
await f.element('ccApplyVolume').onclick();
assert.deepEqual(f.calls,[['/api/volume',{level:.65},'frame-a']]);
assert.equal(f.refreshes,1);
await f.element('ccMute').onclick();
assert.deepEqual(f.calls[1],['/api/volume',{muted:true},'frame-a']);
f.context={online:null,generation:2,device:'frame-b'};f.ui.reset();
assert.equal(f.element('ccApplyVolume').disabled,true);
assert.equal(f.element('metric-battery').textContent,'Unavailable');
assert.equal(f.element('ccAudioResult').textContent,'');
});
test('offline/errors clear live readings, disable audio and keep companion navigation usable', async t => {
const f=mounted();t.after(()=>f.ui.destroy());f.ui.status(sample());
f.context.online=false;f.ui.connection(false);
assert.equal(f.element('metric-fps').textContent,'Unavailable');
assert.equal(f.element('ccVolume').disabled,true);
await f.element('ccMute').onclick();
assert.equal(f.calls.length,0);
assert.match(f.element('ccAudioResult').textContent,/Connect and refresh/);
assert.equal(f.cards.find(c=>c.dataset.ccCard==='privacy').hidden,false);
f.ui.unavailable(Error('Local server is unavailable'));
assert.match(f.element('ccSampleState').textContent,/Local server is unavailable/);
f.context.online=true;f.ui.status(sample());
assert.equal(f.element('ccVolume').disabled,false);
});
test('mounted audio failure is visible and never reports success', async t => {
const f=mounted({api:async()=>{throw Error('Permission denied');}});t.after(()=>f.ui.destroy());f.ui.status(sample());
await f.element('ccApplyVolume').onclick();
assert.match(f.element('ccAudioResult').textContent,/Couldn’t apply audio: Permission denied/);
assert.equal(f.element('ccApplyVolume').disabled,false);
assert.equal(f.refreshes,0);
});
test('audio confirmation depends on fresh readback rather than command acknowledgment', async t => {
let expected=.65, mode='confirmed';
const f=mounted({refresh:async()=>{
if(mode==='offline')return f.ui.unavailable(Error('Headset unreachable'));
const reading=sample();reading.volume.level=mode==='different' ? .64 : expected;f.ui.status(reading);
}});t.after(()=>f.ui.destroy());f.ui.status(sample());
f.element('ccVolume').value='65';await f.element('ccApplyVolume').onclick();
assert.match(f.element('ccAudioResult').textContent,/Confirmed: 65%, unmuted/);
mode='different';f.element('ccVolume').value='65';await f.element('ccApplyVolume').onclick();
assert.match(f.element('ccAudioResult').textContent,/reported output differs: 64%/);
mode='offline';await f.element('ccApplyVolume').onclick();
assert.match(f.element('ccAudioResult').textContent,/command sent; current output could not be confirmed/);
assert.equal(f.element('ccApplyVolume').disabled,true);
});
test('pending audio never overwrites the new headset with old success or error', async t => {
let complete;
const f=mounted({api:()=>new Promise(resolve=>{complete=resolve;})});t.after(()=>f.ui.destroy());f.ui.status(sample());
const pending=f.element('ccMute').onclick();
assert.equal(f.element('ccApplyVolume').disabled,true);
f.context={online:true,generation:2,device:'frame-b'};f.ui.reset();
const newSample=sample();newSample.hostname='Second Frame';newSample.volume.muted=true;f.ui.status(newSample);
complete({message:'Old headset muted'});await pending;
assert.equal(f.element('ccDeviceName').textContent,'Second Frame');
assert.equal(f.element('ccAudioResult').textContent,'');
assert.equal(f.refreshes,0);
assert.equal(f.element('ccMute').textContent,'Unmute');
f.ui.unavailable({stale:true,message:'Switched headsets'});
assert.equal(f.element('metric-battery').textContent,'99%');
assert.equal(f.element('ccMute').disabled,false);
});
test('pending old-headset volume cannot become a draft on the new headset', async t => {
for(const failure of [false,true]) {
const request=deferred(),f=mounted({api:()=>request.promise});t.after(()=>f.ui.destroy());
f.ui.status(sample());f.element('ccVolume').value='65';f.element('ccVolume').oninput();
const pending=f.element('ccApplyVolume').onclick();
f.context={online:true,generation:2,device:'frame-b'};f.ui.reset();
const next=sample();next.volume.level=.2;f.ui.status(next);
assert.equal(f.element('ccApplyVolume').disabled,true);
if(failure)request.reject(Error('Old headset failed'));else request.resolve({message:'Done'});
await pending;
assert.equal(f.element('ccVolume').value,20);
assert.equal(f.element('ccVolumeValue').textContent,'20%');
assert.equal(f.element('ccAudioReadback').textContent,'Reported 20% · unmuted');
assert.equal(f.element('ccApplyVolume').disabled,false);
assert.equal(f.element('ccAudioResult').textContent,'');
}
});
test('production host response works before and after mounting with the actual bridge shape', async t => {
for(const early of [true,false]) {
const f=sharedPage();let ui;
if(!early){ui=f.mount();t.after(()=>ui.destroy());assert.equal(f.element('macLink').hidden,true);}
f.host.resolve({os:'macOS',mobile:false});await flush();
if(early){ui=f.mount();t.after(()=>ui.destroy());}
assert.equal(f.context.watched,true);
assert.equal(f.element('macLink').hidden,false);
f.element('ccSearch').value='mac';f.element('ccSearch').input();
assert.equal(f.element('macLink').hidden,false);
ui.host({os:'SteamOS',mobile:true});assert.equal(f.element('macLink').hidden,true);
}
});
test('production shared refresh drains a pre-command request then samples after acknowledgment', async t => {
const f=sharedPage(),ui=f.mount();t.after(()=>ui.destroy());ui.status(sample());
const prior=f.refresh();assert.equal(f.requests.length,1);
assert.equal(f.refresh(),prior); // the ordinary poll/manual deduplication still holds
f.element('ccVolume').value='65';f.element('ccVolume').oninput();
const command=f.element('ccApplyVolume').onclick();await flush();
assert.equal(f.writes.length,1);assert.equal(f.requests.length,1);
f.requests[0].resolve(sample());await prior;await flush();
assert.equal(f.requests.length,2,'readback must issue a real post-command status request');
assert.doesNotMatch(f.element('ccAudioResult').textContent,/Confirmed|reported output differs/);
assert.equal(f.element('ccApplyVolume').disabled,true);
const after=sample();after.volume.level=.65;f.requests[1].resolve(after);await command;
assert.match(f.element('ccAudioResult').textContent,/Confirmed: 65%, unmuted/);
assert.equal(f.element('ccApplyVolume').disabled,false);
});
test('production audio readback survives an old poll failure and treats its own failed read as unconfirmed', async t => {
for(const failedReadback of [false,true]) {
const f=sharedPage(),ui=f.mount();t.after(()=>ui.destroy());ui.status(sample());
const prior=f.refresh();f.element('ccVolume').value='65';f.element('ccVolume').oninput();
const command=f.element('ccApplyVolume').onclick();await flush();
f.requests[0].reject(Error('Pre-command poll failed'));await prior;await flush();
assert.equal(f.requests.length,2);
assert.equal(f.element('ccApplyVolume').disabled,true);
if(failedReadback)f.requests[1].reject(Error('Readback unavailable'));
else {const after=sample();after.volume.level=.64;f.requests[1].resolve(after);}
await command;
assert.match(f.element('ccAudioResult').textContent,failedReadback ? /command sent; current output could not be confirmed/ : /reported output differs: 64%/);
assert.equal(f.element('ccApplyVolume').disabled,failedReadback);
}
});
test('production Flatpak redraw cannot revive a failed status; only a fresh successful poll recovers', async t => {
const f=sharedPage(),ui=f.mount();t.after(()=>ui.destroy());
const first=f.refresh(),reading=sample();f.requests[0].resolve(reading);await first;
const receipt=vm.runInContext('stateReceivedAt',f.context);
assert.equal(f.element('ccApplyVolume').disabled,false);
const failed=f.refresh();f.requests[1].reject(Error('Synthetic status parsing error'));await failed;
assert.equal(f.element('metric-battery').textContent,'Unavailable');
assert.equal(f.element('ccApplyVolume').disabled,true);
f.installFlatpak(); // actual production render(state), with job held pending
assert.equal(f.requests.length,2,'redraw must not start a replacement reading');
assert.equal(vm.runInContext('stateReceivedAt',f.context),receipt);
assert.equal(f.element('metric-battery').textContent,'Unavailable');
assert.equal(f.element('ccStatusLabel').textContent,'No live sample');
assert.match(f.element('ccSampleState').textContent,/Synthetic status parsing error/);
for(const id of ['ccVolume','ccApplyVolume','ccMute'])assert.equal(f.element(id).disabled,true);
const fresh=f.refresh();
assert.equal(f.element('ccApplyVolume').disabled,true,'starting a poll does not clear failure');
const next=sample();next.battery.percent=98;next.volume.level=.2;
f.requests[2].resolve(next);await fresh;
assert.equal(f.element('metric-battery').textContent,'98%');
assert.equal(f.element('ccVolume').value,20);
assert.equal(f.element('ccApplyVolume').disabled,false);
assert.doesNotMatch(f.element('ccSampleState').textContent,/Synthetic status parsing error/);
});
test('delayed production bootstrap retains failed-read state and fresh-poll recovery', async t => {
for(const priorSuccess of [true,false]) {
const f=sharedPage();
if(priorSuccess){const first=f.refresh();f.requests[0].resolve(sample());await first;}
const failed=f.refresh();f.requests.at(-1).reject(Error('Status failed before mount'));await failed;
const calls=f.requests.length,ui=f.mount();t.after(()=>ui.destroy());
assert.equal(f.requests.length,calls);
assert.equal(f.element('metric-battery').textContent,'Unavailable');
assert.equal(f.element('ccStatusLabel').textContent,'No live sample');
assert.match(f.element('ccSampleState').textContent,/Status failed before mount/);
for(const id of ['ccVolume','ccApplyVolume','ccMute'])assert.equal(f.element(id).disabled,true);
const fresh=f.refresh();assert.equal(f.element('ccApplyVolume').disabled,true);
f.requests.at(-1).resolve(sample());await fresh;
assert.equal(f.element('metric-battery').textContent,'99%');
assert.equal(f.element('ccApplyVolume').disabled,false);
assert.doesNotMatch(f.element('ccSampleState').textContent,/Status failed before mount/);
}
});
test('same-object replay preserves invalidation and receipt age after a failed reading', t => {
const f=mounted();t.after(()=>f.ui.destroy());const reading=sample();
f.ui.status(reading);f.ui.unavailable(Error('Status failure'));
f.ui.status(reading,Date.now()+1000);
assert.equal(f.element('metric-battery').textContent,'Unavailable');
assert.equal(f.element('ccApplyVolume').disabled,true);
assert.match(f.element('ccSampleState').textContent,/Status failure/);
f.ui.status(sample());assert.equal(f.element('ccApplyVolume').disabled,false);
});
test('delayed production bootstrap preserves client receipt age despite headset clock skew', async t => {
const original=Date.now;let now=1000000;Date.now=()=>now;t.after(()=>{Date.now=original;});
const f=sharedPage(),prior=f.refresh();const reading=sample();reading.time-=3600;
f.requests[0].resolve(reading);await prior;
now+=91000;const ui=f.mount();t.after(()=>ui.destroy());
assert.equal(f.element('metric-battery').textContent,'Unavailable');
assert.equal(f.element('ccApplyVolume').disabled,true);
assert.match(f.element('ccSampleState').textContent,/expired/);
const fresh=f.refresh();f.requests[1].resolve(sample());await fresh;
assert.equal(f.element('ccApplyVolume').disabled,false);
assert.equal(f.element('metric-battery').textContent,'99%');
});
test('sample replay cannot renew receipt age; fresh receipts tolerate missing or skewed headset time', t => {
const original=Date.now;let now=1000000;Date.now=()=>now;t.after(()=>{Date.now=original;});
const f=mounted();t.after(()=>f.ui.destroy());const reading=sample();f.ui.status(reading);
now+=91000;f.ui.status(reading);
assert.equal(f.element('ccApplyVolume').disabled,true);
assert.equal(f.element('metric-battery').textContent,'Unavailable');
for(const time of [undefined,null,'invalid',NaN,Infinity,now/1000-3600,now/1000+3600]) {
f.ui.status({...sample(),time});
assert.equal(f.element('ccApplyVolume').disabled,false);
assert.doesNotMatch(f.element('ccSampleState').textContent,/Invalid Date/);
}
for(const receipt of [null,NaN,Infinity,now+1000,now-91000]) {
f.ui.status(sample(),receipt);assert.equal(f.element('ccApplyVolume').disabled,true);
}
});
test('mute-only readback preserves an unapplied volume until Apply sends it', async t => {
let reading=sample();const f=mounted({api:async(path,change,device)=>{
f.calls.push([path,change,device]);reading={...reading,volume:{...reading.volume,...change}};return {message:'Done'};
},refresh:async()=>f.ui.status({...reading})});t.after(()=>f.ui.destroy());f.ui.status(reading);
f.element('ccVolume').value='65';f.element('ccVolume').oninput();
await f.element('ccMute').onclick();
assert.deepEqual(f.calls[0],['/api/volume',{muted:true},'frame-a']);
assert.equal(f.element('ccVolume').value,'65');assert.equal(f.element('ccVolumeValue').textContent,'65%');
assert.match(f.element('ccAudioReadback').textContent,/Reported 40% · muted/);
assert.match(f.element('ccAudioResult').textContent,/Confirmed: 40%, muted/);
await f.element('ccApplyVolume').onclick();
assert.deepEqual(f.calls[1],['/api/volume',{level:.65},'frame-a']);
assert.equal(f.element('ccVolume').value,65);
assert.match(f.element('ccAudioResult').textContent,/Confirmed: 65%, muted/);
});
test('live samples expire even when the status poll is paused', t => {
const original=Date.now;let now=100000;Date.now=()=>now;t.after(()=>{Date.now=original;});
const f=mounted();t.after(()=>f.ui.destroy());f.ui.status(sample());
now+=91000;
// A connection update must also re-check freshness before enabling a control.
f.ui.connection(true);
assert.equal(f.element('ccApplyVolume').disabled,true);
});
test('mounted filters show empty state, counts, reset and host-dependent features', t => {
const f=mounted();t.after(()=>f.ui.destroy());
f.element('ccSearch').value='dpi';f.element('ccSearch').input();
assert.equal(f.cards.find(c=>c.dataset.ccCard==='display').hidden,false);
assert.equal(f.element('ccResultCount').textContent,'1 section');
f.element('ccSearch').value='nothing-matches-this';f.element('ccSearch').input();
assert.equal(f.element('ccEmpty').hidden,false);
f.element('ccResetFilter').onclick();assert.equal(f.element('ccEmpty').hidden,true);
f.ui.host({os:'Windows'});assert.equal(f.element('macLink').hidden,true);
assert.equal(f.cards.find(c=>c.dataset.ccCard==='mac').hidden,false);
f.ui.host({os:'macOS'});assert.equal(f.element('macLink').hidden,false);
f.ui.host({os:'SteamOS',mobile:true});assert.equal(f.element('macLink').hidden,true);
f.element('connectionLink').onclick({preventDefault(){}});assert.equal(f.opened,1);
});
test('every linked workflow selects its real page and section, preserving shortcuts', () => {
const html=fs.readFileSync(require.resolve('../ui/index.html'),'utf8');
const start=html.indexOf('const PAGES ='),end=html.indexOf('window.addEventListener("hashchange", showPage);',start);
const ids=new Set([...html.matchAll(/\bid="([^"]+)"/g)].map(m=>m[1]));
let activePage,scrolled;
const nodes=['home','games','android','tools','devices','control'].map(page=>({dataset:{page},classList:{toggle(key,on){if(on)activePage=page;}}}));
const sections=new Map();
const context={location:{hash:'#control'},document:{querySelectorAll:s=>s==='.page'?nodes:[],title:''},
window:{scrollTo(){}},$:id=>{
if(!ids.has(id))return null;
if(!sections.has(id))sections.set(id,{hidden:false,scrollIntoView(){scrolled=id;}});
return sections.get(id);
},live:false,toggleLive(){},loadMacView(){},loadPanelSwitcher(){}};
const androidStart=html.indexOf('function showAndroidTab(');
const androidEnd=html.indexOf("document.querySelectorAll('[data-store-tab]').forEach(button => button.onclick",androidStart);
vm.createContext(context);vm.runInContext(html.slice(androidStart,androidEnd)+html.slice(start,end)+'\nPAGES.push("devices", "control");',context);
vm.runInContext('showPage()',context);assert.equal(activePage,'control');
const expected={view:'home',shots:'home',vrTools:'home',comfort:'home',pad:'home',library:'games',getgames:'games',sideloaded:'games',android:'android',display:'android',androidLibrary:'android',appStore:'android',
power:'tools',transfer:'tools',media:'tools',apps:'tools',artworkSettings:'tools',privacy:'tools',panels:'tools',macview:'tools',devices:'devices'};
for(const item of cc.catalog) for(const [href] of item.links||[]) {
if(!href.startsWith('#')||href==='#connection')continue;
const id=href.slice(1);assert.ok(ids.has(id)||['android','devices'].includes(id),href);
context.location.hash=href;scrolled=null;vm.runInContext('showPage()',context);
assert.equal(activePage,expected[id],href);
if(!['android','devices'].includes(id))assert.equal(scrolled,id,href);
if(id==='display'||id==='androidLibrary') { assert.equal(sections.get('androidLibrary').hidden,false);assert.equal(sections.get('appStore').hidden,true); }
if(id==='appStore') { assert.equal(sections.get('appStore').hidden,false);assert.equal(sections.get('androidLibrary').hidden,true); }
}
assert.equal(vm.runInContext('PAGES[4]',context),'devices');
assert.equal(vm.runInContext('PAGES[5]',context),'control');
});