Spaces:
Running
Running
File size: 7,552 Bytes
ce0ef24 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | #!/usr/bin/env node
'use strict';
// Execute the real inline dashboard script with a small DOM/fetch harness. No live API.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = process.argv[2] || __dirname;
const html = fs.readFileSync(path.join(root, 'index.html'), 'utf8');
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1];
const ids = [...html.matchAll(/id="([^"]+)"/g)].map(m => m[1]);
const OLD = {id: 'OLD_CHANNEL', title: 'Old channel'};
const NEW = {id: 'NEW_CHANNEL', title: 'New channel'};
const clone = o => JSON.parse(JSON.stringify(o));
let passed = 0;
function harness() {
const elements = new Map(ids.map(id => [id, {
hidden: false, disabled: false, value: '', textContent: '', innerHTML: '', focus() {}
}]));
const state = {active: clone(OLD), authorized: clone(NEW), automatic: false, events: [],
requests: [], prompts: [], alerts: [], accept: true, blockCheck: null, failCheck: false};
const storage = new Map();
const sandbox = {
console, TextEncoder, Date, JSON, String, Number, Math, Promise,
btoa: s => Buffer.from(s, 'binary').toString('base64'),
sessionStorage: {getItem: k => storage.get(k) || null, setItem: (k,v) => storage.set(k,v), removeItem: k => storage.delete(k)},
document: {hidden: false, activeElement: null,
getElementById(id) {assert(elements.has(id), 'missing DOM element: ' + id);return elements.get(id)},
addEventListener() {}},
setInterval(fn) {state.timer = fn;return 1},
alert(text) {state.alerts.push(text)},
confirm(text) {state.prompts.push(text);return state.accept},
async fetch(url, opts) {
const body = opts.body ? JSON.parse(opts.body) : null;
state.requests.push({url, method: opts.method, body, headers: opts.headers});
let data, status = 200;
if (url === '/api/youtube') data = {channel: clone(state.active), automatic: state.automatic,
configured: true, events: clone(state.events), settings: {privacy:'public', title:'title', description:'description'}};
else if (url === '/api/state') data = {tasks:[], logs:[], monitor_running:true, check_interval:20};
else if (url === '/api/youtube/check') {
if (state.blockCheck) await state.blockCheck;
if (state.failCheck) {status=400;data={detail:'OAuth check failed'}}
else data={channel:Object.assign(clone(state.authorized),
state.active.id!==state.authorized.id ? {previous:clone(state.active)} : {})};
} else if (url === '/api/youtube/start' || url === '/api/youtube/channel') {
assert.equal(body.channel_id, state.authorized.id, 'must send the freshly checked channel id');
state.active=clone(state.authorized);state.automatic=url.endsWith('/start');
data={ok:true, channel:clone(state.active)};
} else if (url === '/api/youtube/stop') {state.automatic=false;data={ok:true}}
else throw Error('Unexpected route: '+url);
return {ok:status<400,status,async json(){return clone(data)}};
}
};
const context=vm.createContext(sandbox);
vm.runInContext(script, context);
vm.runInContext("authHeader='Basic dGVzdDp0ZXN0'", context);
return {state, elements, context,
async click(id){await vm.runInContext("$('"+id+"').onclick()",context)},
async refresh(){await vm.runInContext('refresh()',context)},
read(expr){return vm.runInContext(expr,context)}};
}
function pass(name){passed++;console.log('PASS '+name)}
(async()=>{
let h=harness();
h.state.events=[{username:'muhajer_iraqi',title:'Live event',phase:'live',youtube_status:'live',
source:'hd 720p',send_mode:'copy',pending_send_mode:'matchgop',output_seconds:60}];
await h.refresh();
assert.match(h.elements.get('events').innerHTML,/وضع الإرسال الفعلي: copy/);
assert.match(h.elements.get('events').innerHTML,/الترميز الثابت ينتظر خانة: matchgop/);
pass('Per-event sender mode is shown instead of hiding an account-specific fallback behind the global preference');
await h.click('ytcheck');
assert.equal(h.read('checkedChannel.id'),NEW.id);
assert.equal(h.state.active.id,OLD.id);
await h.refresh();await h.state.timer();
assert.equal(h.read('checkedChannel.id'),NEW.id);
assert.equal(h.read('lastY.channel.id'),OLD.id);
assert.equal(h.elements.get('ytadopt').hidden,false);
assert.match(h.elements.get('ytchannelnotice').textContent,/New channel/);
pass('OAuth check → refresh → timer retains NEW, not the journal OLD (production regression)');
await h.click('ytstart');
const posts=h.state.requests.filter(r=>r.method==='POST');
assert.equal(posts.at(-2).url,'/api/youtube/check');
assert.equal(posts.at(-1).url,'/api/youtube/start');
assert.equal(posts.at(-1).body.channel_id,NEW.id);
assert.match(h.state.prompts.at(-1),/Old channel/);
assert.match(h.state.prompts.at(-1),/New channel/);
assert.equal(h.state.automatic,true);
assert.equal(h.elements.get('ytadopt').hidden,true);
pass('Start rechecks OAuth and explicitly confirms both channel names before NEW is enabled');
h=harness();await h.click('ytcheck');await h.click('ytadopt');
assert.equal(h.state.active.id,NEW.id);
assert.equal(h.state.automatic,false);
assert(!h.state.requests.some(r=>r.url==='/api/youtube/start'));
assert.equal(h.elements.get('ytadopt').hidden,true);
pass('Adopt-only confirms the new channel without starting a broadcast');
h=harness();await h.click('ytcheck');h.state.accept=false;await h.click('ytstart');
assert.equal(h.state.active.id,OLD.id);
assert(!h.state.requests.some(r=>r.url==='/api/youtube/start'));
pass('Cancelled start never sends a start request or adopts the channel');
h=harness();await h.click('ytcheck');h.state.accept=false;await h.click('ytadopt');
assert(!h.state.requests.some(r=>r.url==='/api/youtube/channel'));
assert.equal(h.state.active.id,OLD.id);
pass('Cancelled adoption leaves the previous channel unchanged');
h=harness();await h.click('ytcheck');
h.state.authorized={id:'THIRD_CHANNEL',title:'Rotated channel'};
await h.click('ytstart');
assert.equal(h.state.active.id,'THIRD_CHANNEL');
assert.match(h.state.prompts.at(-1),/Rotated channel/);
pass('Secrets rotated after check are freshly discovered, not silently confirmed with a stale id');
h=harness();h.state.failCheck=true;await h.click('ytstart');
assert(!h.state.requests.some(r=>r.url==='/api/youtube/start'));
assert.match(h.elements.get('error').textContent,/OAuth check failed/);
assert.equal(h.elements.get('ytcheck').disabled,false);
pass('Failed OAuth check cannot start and restores the controls');
h=harness();let release;
h.state.blockCheck=new Promise(resolve=>{release=resolve});
const first=h.click('ytstart');await Promise.resolve();await h.click('ytstart');release();await first;
assert.equal(h.state.requests.filter(r=>r.url==='/api/youtube/start').length,1);
assert.equal(h.state.requests.filter(r=>r.url==='/api/youtube/check').length,1);
pass('Double-click while OAuth is pending sends only one check and one start');
h=harness();await h.click('ytcheck');await h.click('logout');
assert.equal(h.read('checkedChannel'),null);
assert.equal(h.read('authHeader'),'');
assert.equal(h.elements.get('ytadopt').hidden,true);
pass('Logout clears the checked channel and credentials');
console.log('\n'+passed+' dashboard regressions passed');
})().catch(err=>{console.error(err);process.exitCode=1});
|