const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'); const source=fs.readFileSync('web/households.js','utf8'); test('leadership choices include enabled local members and exclude unrelated or global accounts',()=>{ const context=vm.createContext({}); vm.runInContext(source.slice(source.indexOf('function leadershipCandidates('),source.indexOf('function syncLeadershipSelection(')),context); const rows=[{id:'a',familyId:'home',role:'member'},{id:'b',familyId:'home',role:'family_admin'},{id:'c',familyId:'home',role:'admin'},{id:'d',familyId:'else',role:'family_admin'},{id:'e',familyId:'home',role:'member',disabled:true}]; assert.equal(JSON.stringify(context.leadershipCandidates(rows,'home').map(x=>x.id)),JSON.stringify(['a','b'])); }); test('empty leadership choices have a member creation path and explicit grant control',()=>{ const markup=fs.readFileSync('frontend/src/components/RuntimeOverlays.vue','utf8'); for(const id of ['leadership-empty','leadership-scope','leadership-new','leadership-members','leadership-grant'])assert.ok(markup.includes(`id="${id}"`)); assert.match(source,/grantManagement=f\.elements\.grantManagement\.checked/); assert.match(source,/disabled=!leadershipUsers.length/); assert.match(source,/grantManagement\.checked=false/); const family=fs.readFileSync('web/family.js','utf8');assert.match(family,/await openLeadership\(returnTo\)/);assert.match(family,/leadershipReturnDraft/); }); function fullscreenFixture(native){ const classes=new Set(),stage={classList:{contains:k=>classes.has(k),add:k=>classes.add(k),remove:k=>classes.delete(k)}}; if(native)stage.requestFullscreen=async()=>{document.fullscreenElement=stage}; const button={setAttribute(k,v){this[k]=v}},document={fullscreenElement:null,body:{style:{overflow:'auto'},classList:{toggle(){}}},addEventListener(){},exitFullscreen:async()=>{document.fullscreenElement=null}}; const context=vm.createContext({document,$:id=>id==='#page-live'?stage:button}); const monitor=fs.readFileSync('web/monitor.js','utf8'); vm.runInContext(monitor.slice(monitor.indexOf("let wallBodyOverflow="),monitor.indexOf('setInterval(')),context); return {context,document,stage,button,classes}; } test('phone fullscreen falls back to viewport and restores scroll on exit',async()=>{ const f=fullscreenFixture(false);await f.button.onclick();assert.ok(f.classes.has('wall-expanded'));assert.equal(f.document.body.style.overflow,'hidden');assert.equal(f.button['aria-pressed'],'true'); f.context.closeWallFullscreen();assert.equal(f.classes.size,0);assert.equal(f.document.body.style.overflow,'auto');assert.equal(f.button['aria-pressed'],'false'); }); test('native fullscreen remains supported and exits without rebuilding video',async()=>{ const f=fullscreenFixture(true);await f.button.onclick();assert.equal(f.document.fullscreenElement,f.stage);assert.equal(f.classes.size,0);await f.button.onclick();assert.equal(f.document.fullscreenElement,null); const app=fs.readFileSync('web/app.js','utf8');assert.match(app,/page!=='live'.*closeWallFullscreen\(\)/); });