const {chromium} = require('playwright'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const views = { 'strategy-roadmap': ['skills','autonomy'], 'strategy-roadmap-adoption': ['delivery','remaining'], 'strategy-roadmap-loop': ['understand','baseline','topology','use','collect','reassess'], }; (async () => { const base = process.argv[2] || 'http://127.0.0.1:4173'; const output = `.cache/roadmap-timeline-${process.argv[3] || 'local'}`; fs.mkdirSync(output, {recursive:true}); const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']}); const results = []; const inspect = async (frame, name, vertical) => { await frame.locator('svg[role="img"]').waitFor(); const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({ width:innerWidth, height:innerHeight, edges:[...svg.querySelectorAll('path[data-edge-id]')].map(p => ({id:p.dataset.edgeId,box:p.getBoundingClientRect().toJSON()})), cards:[...svg.querySelectorAll('[data-node-id]')].map(node => ({ id:node.dataset.nodeId, box:node.querySelector('rect').getBoundingClientRect().toJSON(), number:node.querySelector('[data-stage-number]').textContent, text:[...node.querySelectorAll('text')].map(text => ({value:text.textContent, box:text.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(text).fontSize)*text.getScreenCTM().a})) })) })); const ids=views[name]; geometry.cards.sort((a,b)=>ids.indexOf(a.id)-ids.indexOf(b.id)); assert.deepEqual(geometry.cards.map(n => n.id), ids); assert.deepEqual(geometry.cards.map(n => n.number), ids.length === 6 ? ids.map((_,i)=>`Step ${i+1}`) : name === 'strategy-roadmap' ? ['Stage 1','Stage 2'] : ['First','If justified']); assert.equal(geometry.edges.length,ids.length===6?6:1); if(ids.length===6) assert(geometry.edges.some(e=>e.id==='repeat')); for(const edge of geometry.edges) assert(edge.box.left>=0 && edge.box.right<=geometry.width+1 && edge.box.top>=0 && edge.box.bottom<=geometry.height+1, `clipped edge ${name} ${edge.id}`); for (const [index, card] of geometry.cards.entries()) { assert.equal(card.text.length,3); assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width+1 && card.box.bottom <= geometry.height+1, `outside iframe ${name} ${card.id}`); for(const text of card.text) { assert(text.size>=13, `small text ${name} ${card.id}: ${text.size}`); assert(text.box.left>=card.box.left && text.box.right<=card.box.right+1 && text.box.top>=card.box.top && text.box.bottom<=card.box.bottom+1, `clipped text ${name} ${card.id}`); } for(let t=1;terrors.push(e.message)); assert.equal((await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`,{waitUntil:'networkidle'})).status(),200); await page.addStyleTag({content:'html {scroll-behavior:auto !important;}'}); assert.equal(await page.locator('.archify-panel--story').count(),3); for(const name of Object.keys(views)) { const panel=page.locator(`[data-diagram="${name}"]`); const iframe=panel.locator(width<1100?'.story-mobile':'.story-desktop'); await iframe.scrollIntoViewIfNeeded(); const frame=await (await iframe.elementHandle()).contentFrame(); await frame.locator('svg[role="img"]').waitFor(); const opener=panel.getByRole('button',{name:/^Expand /}); for(const theme of ['light','dark']) { const frameTop=await iframe.evaluate(n=>n.getBoundingClientRect().top+scrollY); await page.evaluate(y=>scrollTo({top:y-120,behavior:'instant'}),frameTop); if(await frame.locator('html').getAttribute('data-theme')!==theme) { await frame.locator('#btn-theme').focus(); await page.keyboard.press('Enter'); } await page.waitForFunction(t=>document.documentElement.dataset.theme===t,theme); const inline=await inspect(frame,name,width<1100); await frame.locator('#btn-preset').focus();await page.keyboard.press('Enter'); await frame.locator('[data-preset-value="blueprint"]').focus();await page.keyboard.press('Enter'); await frame.waitForFunction(()=>document.documentElement.dataset.preset==='blueprint'); await inspect(frame,name,width<1100); await frame.locator('#btn-preset').focus();await page.keyboard.press('Enter'); await frame.locator('[data-preset-value="classic"]').focus();await page.keyboard.press('Enter'); await frame.waitForFunction(()=>document.documentElement.dataset.preset==='classic'); await frame.locator('#btn-export').focus();await page.keyboard.press('Enter'); await frame.locator('[data-format="svg"]').focus(); const [download]=await Promise.all([page.waitForEvent('download'),page.keyboard.press('Enter')]); assert.equal(await download.failure(),null); const exported=`${output}/${width}-${theme}-${name}.svg`; await download.saveAs(exported); const svg=fs.readFileSync(exported,'utf8'); for(const id of views[name]) assert(svg.includes(`data-node-id="${id}"`)); assert(svg.includes('eldorado-roadmap-typography')); await panel.screenshot({path:`${output}/${width}-${theme}-${name}-inline.png`}); // Scroll each card into the real viewport: tall iframe captures alone // may contain compositor blanks even when DOM geometry is correct. for(const card of inline.cards) { const top=await iframe.evaluate(n=>n.getBoundingClientRect().top+scrollY); await page.evaluate(y=>scrollTo({top:y,behavior:'instant'}),top+card.box.top-100); await page.screenshot({path:`${output}/${width}-${theme}-${name}-${card.id}.png`}); } await opener.focus();await page.keyboard.press('Enter'); const close=panel.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'}); const fullIframe=panel.locator('dialog iframe'); const fullscreen=await (await fullIframe.elementHandle()).contentFrame(); await fullscreen.waitForFunction(t=>document.documentElement.dataset.theme===t,theme); const expanded=await inspect(fullscreen,name,width<1000); await page.screenshot({path:`${output}/${width}-${theme}-${name}-fullscreen.png`}); await close.focus();await page.keyboard.press('Escape'); await fullIframe.waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement)); results.push({width,theme,name,inline,expanded});fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2)); } } assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth));assert.deepEqual(errors,[]); } finally {await context.close();} } assert.equal(results.length,24); for(const viewport of [{width:390,height:667},{width:320,height:568}]) { const context=await browser.newContext({viewport}); try { const page=await context.newPage();await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`,{waitUntil:'networkidle'}); await page.addStyleTag({content:'html {scroll-behavior:auto !important;}'}); for(const name of Object.keys(views)) { const panel=page.locator(`[data-diagram="${name}"]`), opener=panel.getByRole('button',{name:/^Expand /}); await opener.focus();await page.keyboard.press('Enter');const modal=panel.locator('dialog'), close=modal.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'}); const iframe=modal.locator('iframe'), frame=await (await iframe.elementHandle()).contentFrame(); const geometry=await inspect(frame,name,true); assert(await modal.evaluate(n=>n.scrollHeight>n.clientHeight),'short fullscreen must scroll'); for(const card of geometry.cards) { await modal.evaluate((n,y)=>{n.scrollTop=y;},card.box.top); const b=await iframe.boundingBox(), t=await modal.locator('.archify-modal-toolbar').boundingBox(), c=await close.boundingBox(); assert(c.y>=0 && c.y+c.height<=viewport.height); assert(b.y+card.box.top>=t.y+t.height-1 && b.y+card.box.bottom<=viewport.height+1,`unreachable ${name} ${card.id}`); } await modal.evaluate(n=>{n.scrollTop=0;}); for(const selector of ['#btn-theme','#btn-preset','#btn-export']) { const control=frame.locator(selector), b=await control.boundingBox();assert(b.x>=0 && b.x+b.width<=viewport.width && b.y>=0 && b.y+b.height<=viewport.height);await control.click();if(selector!=='#btn-theme')await control.click(); } await close.focus();await page.keyboard.press('Escape');await iframe.waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement)); await page.keyboard.press('Enter');await close.waitFor({state:'visible'});await close.click();await iframe.waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement)); results.push({viewport,name,scrollable:geometry});fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2)); } } finally {await context.close();} } assert.equal(results.length,30); console.log('PASS 48 diagram contexts: three views × four widths × light/dark × inline/fullscreen; plus six short-phone scrolling/keyboard contexts.'); } finally {await browser.close();} })().catch(error=>{console.error(error);process.exitCode=1;});