Files

140 lines
6.7 KiB
JavaScript
Raw Permalink Normal View History

/**
* Verifies the ProPresenter data pipeline used by main.js against tmp/pp-mock.js.
* Mirrors the main-process client logic (stream parsing, slide-count summing,
* active-item matching, NOW/NEXT assembly) and asserts the resulting view-model.
*
* node tmp/pp-verify.js [port]
*/
const HOST = '127.0.0.1';
const PORT = Number.parseInt(process.argv[2] || '1599', 10);
let failures = 0;
function check(label, cond) {
console.log(`${cond ? 'PASS' : 'FAIL'} ${label}`);
if (!cond) failures += 1;
}
// ---- client logic mirrored from main.js ----
const slideCountCache = new Map();
let state = { reachable: false, playlistUuid: '', playlistName: '', items: [], activeUuid: '', activeName: '', activeIndex: -1, currentSlide: 0 };
function ppUrl(target) { return `http://${HOST}:${PORT}${target}`; }
async function ppRequestJson(target, init) {
const r = await fetch(ppUrl(target), { headers: { Accept: 'application/json' }, ...init });
if (!r.ok) throw new Error(`${r.status}`);
const t = await r.text();
return t ? JSON.parse(t) : null;
}
async function ppSlideCount(uuid) {
if (!uuid) return null;
if (slideCountCache.has(uuid)) return slideCountCache.get(uuid);
try {
const data = await ppRequestJson(`/v1/presentation/${encodeURIComponent(uuid)}`);
let count = 0;
for (const g of data?.groups || []) for (const s of g?.slides || []) if (s?.enabled !== false) count += 1;
slideCountCache.set(uuid, count);
return count;
} catch { return null; }
}
function recomputeActiveIndex() {
state.activeIndex = state.activeUuid ? state.items.findIndex((it) => it.uuid && it.uuid === state.activeUuid) : -1;
}
async function refreshCurrentPlaylist() {
const cur = await ppRequestJson('/v1/playlist/current');
state.playlistUuid = String(cur?.uuid || '');
state.playlistName = String(cur?.name || '');
if (!state.playlistUuid) { state.items = []; recomputeActiveIndex(); return; }
const data = await ppRequestJson(`/v1/playlist/${encodeURIComponent(state.playlistUuid)}`);
state.items = (data?.items || []).map((it) => ({
uuid: String(it?.id?.uuid || ''), name: String(it?.id?.name || ''), type: String(it?.type || ''),
isHidden: Boolean(it?.is_hidden), isPco: Boolean(it?.is_pco), slideCount: null,
}));
recomputeActiveIndex();
}
async function fillSlideCounts() {
for (const it of state.items) if (it.type === 'presentation' && it.uuid && it.slideCount === null) it.slideCount = await ppSlideCount(it.uuid);
}
function applySlideIndex(data) {
const pres = data?.presentation;
if (!pres || !pres.presentation_id) { state.activeUuid = ''; state.activeName = ''; state.currentSlide = 0; recomputeActiveIndex(); return; }
state.currentSlide = Number.isInteger(pres.index) ? pres.index + 1 : 0;
state.activeUuid = String(pres.presentation_id.uuid || '');
state.activeName = String(pres.presentation_id.name || '');
recomputeActiveIndex();
}
async function viewModel() {
let total = 0;
if (state.activeUuid) { const c = slideCountCache.get(state.activeUuid); total = Number.isInteger(c) ? c : (await ppSlideCount(state.activeUuid)) || 0; }
return { reachable: state.reachable, playlistName: state.playlistName, items: state.items, activeIndex: state.activeIndex, activePresentationName: state.activeName, currentSlide: state.currentSlide, currentSlideTotal: total };
}
function describe(vm) {
const now = vm.activeIndex >= 0 ? vm.items[vm.activeIndex].name : vm.activePresentationName;
const from = vm.activeIndex >= 0 ? vm.activeIndex + 1 : 0;
const next = vm.items.slice(from).filter((it) => !it.isHidden)
.map((it) => it.type === 'presentation' ? `${it.name} (${it.slideCount})` : `${it.name} [${it.type}]`);
return `NOW: ${now} — slide ${vm.currentSlide}/${vm.currentSlideTotal}\n NEXT: ${next.join(' · ')}`;
}
async function main() {
await refreshCurrentPlaylist();
applySlideIndex(await ppRequestJson('/v1/presentation/slide_index'));
await fillSlideCounts();
state.reachable = true;
const vm = await viewModel();
console.log('\n--- initial view-model ---\n' + describe(vm) + '\n');
const byName = (n) => vm.items.find((it) => it.name === n);
check('playlist name', vm.playlistName === 'Sunday AM — June 1');
check('item count = 8', vm.items.length === 8);
check('Welcome slides = 3', byName('Welcome').slideCount === 3);
check('Amazing Grace slides = 12', byName('Amazing Grace').slideCount === 12);
check('Sermon slides = 5', byName('Sermon: Living Hope').slideCount === 5);
check('Closing slides = 8', byName('Closing Song').slideCount === 8);
check('Benediction slides = 2', byName('Benediction').slideCount === 2);
check('media item has no count', byName('Bumper Video').slideCount === null);
check('header item has no count', byName('Pre-Service Loop').slideCount === null);
check('active is Amazing Grace', vm.activeIndex === vm.items.findIndex((it) => it.name === 'Amazing Grace'));
check('current slide >= 1', vm.currentSlide >= 1);
check('current total = 12', vm.currentSlideTotal === 12);
const from = vm.activeIndex + 1;
const nextNames = vm.items.slice(from).filter((it) => !it.isHidden).map((it) => it.name);
check('NEXT excludes hidden cue', !nextNames.includes('Hidden Cue'));
check('NEXT starts with Sermon', nextNames[0] === 'Sermon: Living Hope');
// Now exercise the live chunked stream for a few updates.
console.log('\n--- streaming live updates ---');
setTimeout(() => { console.error('TIMEOUT waiting for stream updates'); process.exit(3); }, 8000);
const res = await fetch(ppUrl('/v1/status/updates'), {
method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(['presentation/slide_index', 'playlist/current']),
});
check('stream opened (chunked)', res.ok && !!res.body);
let updates = 0;
const seenSlides = [];
let buffer = '';
const decoder = new TextDecoder();
for await (const chunk of res.body) {
buffer += decoder.decode(chunk, { stream: true });
let nl;
while ((nl = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, nl).trim();
buffer = buffer.slice(nl + 1);
if (!line) continue;
const msg = JSON.parse(line);
if (String(msg.url).includes('presentation/slide_index')) {
applySlideIndex(msg.data);
seenSlides.push(state.currentSlide);
console.log(describe(await viewModel()).split('\n')[0]);
updates += 1;
}
}
if (updates >= 4) break;
}
check('received >= 4 live slide updates', updates >= 4);
check('slide value changed across updates', new Set(seenSlides).size >= 2);
console.log(`\n${failures === 0 ? '✅ ALL CHECKS PASSED' : `❌ ${failures} CHECK(S) FAILED`}`);
process.exit(failures === 0 ? 0 : 1);
}
main().catch((e) => { console.error('verify error:', e); process.exit(2); });