main
John Lauer Refresh dashboard project images and captions alongside live timeline; preserve last good view 453cf46
// Exercise the dashboard's actual refresh function with deterministic fetch/image failures.
import fs from 'node:fs';
import vm from 'node:vm';
import assert from 'node:assert/strict';
const html=fs.readFileSync(new URL('../tools/timeline/index.html',import.meta.url),'utf8');
const start=html.indexOf('  let presentation = null;');
const end=html.indexOf("  $('contribute-prompt')",start);
assert(start>0 && end>start);
const elements=new Map();
const $=id=>{if(!elements.has(id))elements.set(id,{hidden:true,removeAttribute(k){delete this[k]}});return elements.get(id)};
let response='{}',status=200,imageFailure=false,loads=0,renders=0;
const ctx={URL,AbortSignal,setTimeout,clearTimeout,location:{href:'http://localhost:8893/',origin:'http://localhost:8893'},$,model:{gate:'unchanged'},render(){renders++},fetch:async()=>({ok:status===200,text:async()=>response}),Image:class{set src(url){loads++;queueMicrotask(()=>imageFailure?this.onerror():this.onload())}}};
vm.createContext(ctx);vm.runInContext(html.slice(start,end)+'\nglobalThis.refresh=refreshPresentation;',ctx);
const put=async data=>{response=JSON.stringify(data);await ctx.refresh()};
await put({image:'before.png',caption:'Before'});assert.equal($('board-caption').textContent,'Before');assert.equal(loads,1);
await ctx.refresh();assert.equal(loads,1,'unchanged metadata must not reload/flicker');
await put({image:'after.png',caption:'Current passed'});assert.equal($('board-caption').textContent,'Current passed');assert.match($('board-image').src,/after.png/);
const previous=$('board-image').src;
response='{';await ctx.refresh();assert.equal($('board-image').src,previous,'partial file preserves view');
status=404;await ctx.refresh();assert.equal($('board-image').src,previous);status=200;
await put({image:'https://tracker.invalid/p.png',caption:'Wrong'});assert.equal($('board-caption').textContent,'Current passed');
await put({image:'script.html',caption:'Wrong'});assert.equal($('board-image').src,previous);
imageFailure=true;await put({image:'missing.png',caption:'Wrong'});assert.equal($('board-image').src,previous);imageFailure=false;
await ctx.refresh();assert.match($('board-image').src,/missing.png/,'failed image is retried');
await put({title:'Requirements',summary:'Before a board exists'});assert.equal($('board-image').hidden,true);assert.equal($('board-caption').hidden,true);assert.equal($('preview-empty').hidden,false);assert.equal($('board-image').src,undefined);
await put({image:'same.png',caption:'One',imageRevision:1});const first=$('board-image').src;await put({image:'same.png',caption:'One',imageRevision:2});assert.notEqual($('board-image').src,first);
assert.equal(ctx.model.gate,'unchanged');assert(renders>=5);
assert(html.includes('await refreshPresentation();'),'poll must refresh even when timeline is unchanged');
console.log('PASS: changed image/caption, unchanged timeline, partial/missing JSON, URL policy, image failure/retry, removal, same-filename revision; gate state untouched');