// Verify endpoint presentation and that starting never saves stray form limits. const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const html = fs.readFileSync(require('node:path').join(__dirname,'../halloween/camera.html'),'utf8'); const state = html.slice(html.indexOf(' function endpointState('),html.indexOf(' async function pollFollow()')); const begin = html.slice(html.indexOf(' async function beginFollow('),html.indexOf(" $('follow-preview').onclick")); const save = html.slice(html.indexOf(' async function saveFollow()'),html.indexOf(" for (const [id,hue]")); const config={search_enabled:false,follow_speed:80,hue:85,tolerance:18,pan_min:1400,pan_max:1600,tilt_min:1400,tilt_max:1600,pan_reverse:false,tilt_reverse:false,pan_setup_min:1100,pan_setup_max:1900,tilt_setup_min:1100,tilt_setup_max:1900}; const nodes={'follow-speed':{value:80}}, calls=[]; const ctx=vm.createContext({ $:id => nodes[id] || (nodes[id]={checked:false,value:18}), setupReady:true, manualLimitsDirty:false, configEpoch:0, followConfig:{...config}, followLatest:null, releaseJog:()=>{}, jogSequence:0, followSession:null, followSample:null, followEpoch:0, api:async (url,body) => {calls.push({url,body}); return url.endsWith('/start') ? {session:'test'} : body;}, fillFollow:c=>{ctx.followConfig=c;} }); vm.runInContext(state+begin+save,ctx); (async()=>{ ctx.sample={config:{...config},pan_us:1100,tilt_us:1500}; vm.runInContext('renderEndpoints(sample)',ctx); assert.equal(nodes['setup-value-left'].textContent,'Gespeichert: 1400 µs'); assert.equal(nodes['setup-save-left'].disabled,false); assert.match(nodes['setup-hint-left'].textContent,/1110/); assert.equal(nodes['setup-save-right'].disabled,true); assert.equal(nodes['setup-save-up'].disabled,true); ctx.sample={config:{...config,pan_reverse:true},pan_us:1100,tilt_us:1900}; vm.runInContext('renderEndpoints(sample)',ctx); assert.equal(nodes['setup-save-left'].disabled,true); assert.equal(nodes['setup-save-right'].disabled,false); assert.equal(nodes['setup-save-down'].disabled,false); ctx.sample={config:{...config,pan_min:1728,pan_max:1800},pan_us:1718,tilt_us:1500,setup_origin:{pan:1750,tilt:1500}}; vm.runInContext('renderEndpoints(sample)',ctx); assert.equal(nodes['setup-save-left'].disabled,false); // above 1500 is valid assert.equal(nodes['setup-save-right'].disabled,true); ctx.sample.pan_us=1751; vm.runInContext('renderEndpoints(sample)',ctx); assert.equal(nodes['setup-save-left'].disabled,true); // original session centre ctx.sample.centering=true; ctx.sample.pan_us=1718; vm.runInContext('renderEndpoints(sample)',ctx); assert.equal(nodes['setup-save-left'].disabled,true); assert.match(nodes['setup-hint-left'].textContent,/Mittelstellungsfahrt/); ctx.sample.centering=false; ctx.setupReady=false; vm.runInContext('renderEndpoints(sample)',ctx); assert.equal(nodes['setup-save-right'].disabled,true); assert.match(nodes['setup-hint-right'].textContent,/Einrichtmodus/); await vm.runInContext("beginFollow(false,'setup')",ctx); assert.deepEqual(calls.map(c=>c.url),['/api/follow/start']); assert.equal(ctx.followSession,'test'); nodes['follow-pan-reverse'].checked=true; await assert.rejects(vm.runInContext("beginFollow(false)",ctx),/zuerst dort speichern/); assert.equal(calls.length,1); nodes['follow-pan-reverse'].checked=false; nodes['follow-speed'].value=400; await assert.rejects(vm.runInContext("beginFollow(false)",ctx),/zuerst dort speichern/); nodes['follow-speed'].value=80; nodes['follow-search'].checked=true; await assert.rejects(vm.runInContext("beginFollow(false)",ctx),/zuerst dort speichern/); nodes['follow-speed'].value=400; nodes['follow-pan-reverse'].checked=true; // A settings save only changes colour/direction/speed, never endpoint values. nodes['follow-pan-min']={value:500}; nodes['follow-pan-max']={value:500}; await vm.runInContext('saveFollow()',ctx); assert.equal(calls[1].body.pan_min,1400); assert.equal(calls[1].body.pan_max,1600); assert.equal(calls[1].body.pan_reverse,true); assert.equal(calls[1].body.follow_speed,400); assert.equal(calls[1].body.search_enabled,true); assert.ok(html.includes('id="follow-pan-min"')); assert.ok(html.includes('Diese Position als Links speichern')); ctx.manualLimitsDirty=true; await assert.rejects(vm.runInContext("beginFollow(false)",ctx),/noch nicht gespeichert/); ctx.manualLimitsDirty=false; const manual=html.slice(html.indexOf(' function fillManualLimits('),html.indexOf(' // Manual limit form bindings.')); vm.runInContext(manual,ctx); vm.runInContext('fillManualLimits(followConfig)',ctx); nodes['follow-pan-min'].value=1200; nodes['follow-pan-max'].value=1800; nodes['follow-tilt-min']={value:1300}; nodes['follow-tilt-max']={value:1700}; await vm.runInContext('saveManualLimits()',ctx); assert.equal(calls.at(-1).body.pan_min,1200); assert.equal(calls.at(-1).body.tilt_max,1700); assert.equal(calls.at(-1).body.pan_reverse,true); assert.match(nodes['manual-limits-status'].textContent,/gespeichert/); assert.equal(calls.at(-1).body.follow_speed,400); assert.equal(calls.at(-1).body.search_enabled,true); nodes['follow-tilt-min'].value=1700; nodes['follow-tilt-max'].value=1800; await vm.runInContext('saveManualLimits()',ctx); assert.equal(calls.at(-1).body.tilt_min,1700); // range need not contain 1500 const count=calls.length; nodes['follow-pan-max'].value=2500; await assert.rejects(vm.runInContext('saveManualLimits()',ctx),/500–2500/); assert.equal(calls.length,count); nodes['follow-pan-setup-max'].value=2500; await vm.runInContext('saveManualLimits()',ctx); assert.equal(calls.at(-1).body.pan_max,2500); assert.equal(calls.at(-1).body.pan_setup_max,2500); nodes['follow-pan-setup-max'].value=2501; await assert.rejects(vm.runInContext('saveManualLimits()',ctx),/500–2500/); const centerCode=html.slice(html.indexOf(' async function centerSetup()'),html.indexOf(" $('setup-center').onclick")); vm.runInContext(centerCode,ctx); ctx.setupReady=false; await assert.rejects(vm.runInContext('centerSetup()',ctx),/Einrichtmodus/); ctx.setupReady=true; ctx.followSession='test'; ctx.jogSequence=5; ctx.api=async (url,body)=>{calls.push({url,body});return {accepted:true};}; ctx.sendJog=async direction=>{calls.push({url:'/api/follow/jog',body:{direction,sequence:++ctx.jogSequence}});}; await vm.runInContext('centerSetup()',ctx); assert.equal(calls.at(-1).url,'/api/follow/center'); assert.equal(calls.at(-1).body.sequence,6); await vm.runInContext('stopSetupMotion()',ctx); assert.equal(calls.at(-1).body.direction,null); assert.equal(calls.at(-1).body.sequence,7); console.log('Endpoint UI tests passed'); })().catch(e=>{console.error(e);process.exit(1);});