${t.render()}`;
art.dataset.populated = '1';
art.classList.add('is-open');
// Animate skill bars if present
$$('.skill__fill', body).forEach(el => requestAnimationFrame(()=> el.style.width = el.dataset.pct + '%'));
// Hook resume buttons
const dl = $('#dl-resume', body); if (dl) dl.addEventListener('click', downloadResume);
const pr = $('#print-resume', body); if (pr) pr.addEventListener('click', ()=>window.print());
}
}
// ============ RUN ENGINE ============
function selectTest(id, scroll){
$$('.test').forEach(r=>r.classList.toggle('is-selected', r.dataset.id===id));
if (scroll) {
const el = $(`#result-${id}`);
if (el) el.scrollIntoView({ behavior:'smooth', block:'start' });
}
}
async function runTest(id){
const t = tests.find(x=>x.id===id);
if (!t) return;
const s = state[id];
if (s.status === 'running') return;
// reset
s.status = 'running'; s.runtime = 0;
refreshTreeRow(id); refreshResultRow(id); updateStatusbar();
selectTest(id, true);
consoleLine('info', `▶ running test('${t.title}')`);
const dur = headed() ? t.duration * 2.5 : t.duration;
const start = performance.now();
const bar = $(`#bar-${id}`);
bar.style.width = '0%';
await tween(dur, (p)=>{ bar.style.width = (p*100) + '%'; });
const elapsed = Math.round(performance.now() - start);
s.runtime = elapsed; s.status = 'passed';
bar.style.width = '100%';
setTimeout(()=>{ bar.style.transition='opacity .4s'; bar.style.opacity='0'; }, 200);
// Clear populated flag so it re-renders fresh on re-run
const art = $(`#result-${id}`); if (art) art.dataset.populated = '';
refreshTreeRow(id); refreshResultRow(id); updateStatusbar();
consoleLine('ok', `✓ test('${t.title}') passed in ${formatMs(elapsed)}`);
}
async function runAll(){
if (isRunningAll) return;
isRunningAll = true;
$('#run-all').disabled = true;
$('#stop-all').disabled = false;
consoleLine('info', `▶ playwright test (workers=1)`);
for (const t of tests) {
const row = $(`.test[data-id="${t.id}"]`);
if (row && row.style.display === 'none') continue; // respect filter
// reset to idle if previously passed
state[t.id].status = 'idle'; state[t.id].runtime = 0;
const art = $(`#result-${t.id}`); if (art) { art.dataset.populated=''; const b=$(`#bar-${t.id}`); if(b){b.style.width='0%';b.style.opacity='1';b.style.transition='';}}
refreshTreeRow(t.id); refreshResultRow(t.id);
await runTest(t.id);
if (!isRunningAll) break;
}
isRunningAll = false;
$('#run-all').disabled = false;
$('#stop-all').disabled = true;
const total = Object.values(state).reduce((a,s)=>a+(s.runtime||0),0);
consoleLine('ok', `Test suite finished — ${countPassed()} passed in ${formatMs(total)}`);
}
function resetAll(){
isRunningAll = false;
for (const t of tests) {
state[t.id] = { status:'idle', runtime:0 };
const art = $(`#result-${t.id}`);
if (art) {
art.dataset.populated = '';
art.classList.remove('is-open');
const body = $(`#body-${t.id}`); if (body) body.innerHTML='';
const b = $(`#bar-${t.id}`); if (b){ b.style.width='0%'; b.style.opacity='1'; b.style.transition=''; }
}
refreshTreeRow(t.id); refreshResultRow(t.id);
}
$('#console').innerHTML = '';
updateStatusbar();
consoleLine('info', 'reset · all tests returned to idle');
}
// ============ STATUS / COUNTS ============
function countPassed(){ return Object.values(state).filter(s=>s.status==='passed').length; }
function updateStatusbar(){
const passed = countPassed();
const running = Object.values(state).filter(s=>s.status==='running').length;
$('#cnt-pass').textContent = passed;
$('#cnt-fail').textContent = 0;
$('#cnt-skip').textContent = 0;
const dot = $('#status-dot'); const txt = $('#status-text');
if (running) { dot.className='dot dot--running'; txt.textContent = 'running'; }
else if (passed === tests.length) { dot.className='dot dot--pass'; txt.textContent='all passed'; }
else if (passed > 0) { dot.className='dot dot--pass'; txt.textContent='partial'; }
else { dot.className='dot dot--idle'; txt.textContent='idle'; }
const total = Object.values(state).reduce((a,s)=>a+(s.runtime||0),0);
$('#sb-runtime').textContent = `runtime: ${formatMs(total)}`;
}
// ============ CONSOLE ============
function consoleLine(lvl, msg){
const el = $('#console');
const ts = new Date().toLocaleTimeString('en-CA', { hour12:false });
const line = document.createElement('div');
line.className = 'console__line';
line.innerHTML = `${ts}${lvl.toUpperCase()}${msg}`;
el.appendChild(line);
el.scrollTop = el.scrollHeight;
}
// ============ TABS ============
function initTabs(){
$$('.tab').forEach(tab=>{
tab.addEventListener('click', ()=>{
const id = tab.dataset.tab;
$$('.tab').forEach(t=>t.classList.toggle('is-active', t===tab));
$$('.pane').forEach(p=>p.classList.toggle('is-active', p.id === `pane-${id}`));
});
});
}
// ============ TRACE PANE ============
function renderTrace(){
// Build a career timeline: each role gets a bar relative to a global timeline
const trace = $('#trace');
const items = data.experience.map(e => {
const m = e.when.match(/(\w+\s+\d{4})\s*[–-]\s*(\w+\s+\d{4})/i);
if (!m) return null;
return { ...e, start: parseMon(m[1]), end: parseMon(m[2]) };
}).filter(Boolean);
const min = Math.min(...items.map(i=>i.start.getTime()));
const max = Math.max(...items.map(i=>i.end.getTime()));
const span = max - min;
trace.innerHTML = items.map(it=>{
const left = ((it.start - min) / span) * 100;
const width = Math.max(2, ((it.end - it.start) / span) * 100);
const months = Math.round((it.end - it.start)/(1000*60*60*24*30));
return `
${_esc(it.company)} · ${_esc(it.role)}
${months}mo
`;
}).join('');
// Axis ticks: years from min to max
const axis = $('#trace-axis');
const yStart = new Date(min).getFullYear();
const yEnd = new Date(max).getFullYear();
const years = [];
for (let y=yStart; y<=yEnd; y+=Math.max(1, Math.ceil((yEnd-yStart)/8))) years.push(y);
if (years[years.length-1] !== yEnd) years.push(yEnd);
axis.innerHTML = `
`).join('');
}
// ============ RESUME DOWNLOAD ============
function downloadResume(){
// Build a single-page HTML resume as a blob, open it in a new tab and trigger print
const p = data.person;
const html = `${p.first} ${p.last} — Resume
${p.first} ${p.last}
${p.email} · ${p.phone}
${p.title} · ${p.location} · ${p.linkedin}
${p.blurb}
Experience
${data.experience.map(e=>`
${e.company} — ${e.role}
${e.when} · ${e.where}
${e.bullets.map(b=>`
${b}
`).join('')}
`).join('')}
Skills
${data.skills.map(s=>`
${s.name.replace(/\*\*(.+?)\*\*/g,'$1')}
`).join('')}
Projects
${data.projects.map(p=>`
${p.name}
${p.desc}
`).join('')}
`;
const blob = new Blob([html], { type:'text/html' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
consoleLine('ok', '⇩ resume.pdf generated — print dialog opened in new tab');
}
// ============ THEME TOGGLE ============
// Persist via cookie (localStorage is unavailable in some sandboxed iframes)
function readThemeCookie(){
const m = document.cookie.match(/(?:^|;\s*)theme=(dark|light)/);
return m ? m[1] : null;
}
function writeThemeCookie(v){
try { document.cookie = `theme=${v}; path=/; max-age=31536000; SameSite=Lax`; } catch(_) {}
}
function initTheme(){
const saved = readThemeCookie() || 'dark';
document.documentElement.setAttribute('data-theme', saved);
$('#theme-toggle').addEventListener('click', ()=>{
const cur = document.documentElement.getAttribute('data-theme');
const next = cur === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
writeThemeCookie(next);
});
}
// ============ HELPERS ============
function formatMs(ms){
if (ms < 1000) return `${Math.round(ms)}ms`;
return `${(ms/1000).toFixed(1)}s`;
}
function tween(duration, onUpdate){
return new Promise(resolve=>{
const start = performance.now();
function frame(now){
const p = Math.min(1, (now-start)/duration);
onUpdate(p);
if (p < 1) requestAnimationFrame(frame); else resolve();
}
requestAnimationFrame(frame);
});
}
// ============ INIT ============
function init(){
initTheme();
renderTagBar();
renderTree();
renderResults();
renderTrace();
renderSource();
initTabs();
updateStatusbar();
$('#grep').addEventListener('input', applyFilter);
$('#run-all').addEventListener('click', runAll);
$('#stop-all').addEventListener('click', ()=>{ isRunningAll = false; consoleLine('warn','■ stop requested — finishing current test'); });
$('#reset-all').addEventListener('click', resetAll);
$('#expand-all').addEventListener('click', ()=>{
$$('.suite').forEach(s=>s.classList.remove('collapsed'));
});
// Mobile drawer
const sidebar = $('#sidebar');
const scrim = $('#sidebar-scrim');
function closeDrawer(){ sidebar.classList.remove('is-open'); scrim.classList.remove('is-open'); }
$('#menu-btn').addEventListener('click', ()=>{
sidebar.classList.toggle('is-open');
scrim.classList.toggle('is-open');
});
scrim.addEventListener('click', closeDrawer);
// Close drawer when a test is selected on mobile
sidebar.addEventListener('click', e => {
if (window.innerWidth <= 900 && (e.target.closest('.test') || e.target.closest('.test__run'))) {
setTimeout(closeDrawer, 120);
}
});
consoleLine('info', `Running ${tests.length} tests using 1 worker`);
// Friendly nudge: animate the Run All button briefly on first load
setTimeout(()=> $('#run-all').classList.add('pulse'), 600);
}
document.addEventListener('DOMContentLoaded', init);
})();