const {chromium} = require('playwright'); const assert = require('node:assert/strict'); const fs = require('node:fs'); (async () => { const base = process.argv[2] || 'http://127.0.0.1:4173'; const output = `.cache/roadmap-${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 = []; try { // Intermediate widths must use the readable vertical Roadmap, not shrink the loop. for (const width of [768, 1024]) { const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'}); try { const page = await context.newPage(); const errors = []; page.on('pageerror', error => errors.push(error.message)); const response = await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'}); assert.equal(response.status(), 200); const panel = page.locator('.archify-panel--story[data-diagram="strategy-roadmap"]'); const iframe = panel.locator('.story-mobile'); assert(await iframe.isVisible(), `${width}: vertical Roadmap iframe must be visible`); assert(!await panel.locator('.story-desktop').isVisible(), `${width}: horizontal Roadmap must be hidden`); const frame = await (await iframe.elementHandle()).contentFrame(); await frame.locator('svg[role="img"]').waitFor(); const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({ width:innerWidth, height:innerHeight, overflow:document.documentElement.scrollWidth > innerWidth || document.documentElement.scrollHeight > innerHeight, nodes:[...svg.querySelectorAll('[data-node-id]')].map(node => ({ id:node.dataset.nodeId, box:node.querySelector('rect').getBoundingClientRect().toJSON(), text:[...node.querySelectorAll('text')].map(t => ({text:t.textContent, box:t.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(t).fontSize)*t.getScreenCTM().a})) })).sort((a,b) => a.box.y-b.box.y || a.box.x-b.box.x) })); assert.deepEqual(geometry.nodes.map(n => n.id), ['skills','autonomy']); for (const [index, card] of geometry.nodes.entries()) { assert(card.text.length > 0, `${width}: missing text in ${card.id}`); assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width + 1 && card.box.bottom <= geometry.height + 1, `${width}: card outside iframe: ${card.id}`); for (const t of card.text) { assert(t.box.left >= card.box.left && t.box.right <= card.box.right + 1 && t.box.top >= card.box.top && t.box.bottom <= card.box.bottom + 1, `${width}: clipped ${card.id} ${t.text}`); assert(t.size >= 11, `${width}: small ${card.id} ${t.text}: ${t.size}`); } if (index) { const previous = geometry.nodes[index - 1].box; assert.equal(previous.x, card.box.x); assert(previous.bottom < card.box.top, `${width}: stages must flow vertically`); } } assert(!geometry.overflow, `${width}: iframe overflow`); assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), `${width}: page overflow`); assert.deepEqual(errors, []); results.push({width, geometry, responsiveReadability:true}); fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2)); } finally {await context.close();} } for (const width of [1440, 390, 320]) { const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'}); const page = await context.newPage(); const errors = []; page.on('pageerror', error => errors.push(error.message)); const response = await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'}); assert.equal(response.status(), 200); assert.equal(await page.locator('h1').innerText(), 'Roadmap'); const headings = page.locator('.strategy-content > h2'); assert.deepEqual((await headings.allTextContents()).map(s => s.replace(/\u200b/g, '')), ['Six months, two investment decisions','Delivery first, replication only if earned','Keep one learning loop','Solo capacity and influence']); const text = await page.locator('.strategy-content').innerText(); for (const marker of ['Humans direct, guide and review throughout', 'Delivery keeps improving', 'system-logic source', 'baseline', 'Target operating model']) assert(text.includes(marker), marker); assert(!text.includes('Supporting gate evidence and hold conditions\n\nThese are')); if (width < 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click(); const menu = page.locator('.theme-doc-sidebar-menu .menu__link').filter({visible:true}); await menu.first().waitFor({state:'visible'}); assert.deepEqual((await menu.allTextContents()).slice(0,2), ['Target','Roadmap']); if (width < 700) await page.getByRole('button', {name:'Close navigation bar'}).click(); const panel = page.locator('.archify-panel--story').first(); const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile'); await iframe.scrollIntoViewIfNeeded(); const frame = await (await iframe.elementHandle()).contentFrame(); await frame.locator('svg[role="img"]').waitFor(); const checkCards = async () => { const cards = await frame.locator('svg[role="img"] [data-node-id]').evaluateAll(nodes => nodes.map(node => { const box = node.querySelector('rect').getBoundingClientRect(); return {id:node.dataset.nodeId, box:box.toJSON(), text:[...node.querySelectorAll('text')].map(t => ({text:t.textContent, box:t.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(t).fontSize)*t.getScreenCTM().a}))}; })); for (const card of cards) for (const t of card.text) { assert(t.box.left >= card.box.left && t.box.right <= card.box.right + 1 && t.box.top >= card.box.top && t.box.bottom <= card.box.bottom + 1, `${width}: clipped ${card.id} ${t.text}`); assert(t.size >= 11, `${width}: small ${card.id} ${t.text}: ${t.size}`); } return cards; }; await checkCards(); const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({ nodes:[...svg.querySelectorAll('[data-node-id]')].map(n => ({id:n.dataset.nodeId,...n.getBoundingClientRect().toJSON()})).sort((a,b) => a.y-b.y || a.x-b.x), width:innerWidth, height:innerHeight, minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n => parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a)) })); assert.deepEqual(geometry.nodes.map(n => n.id), ['skills','autonomy']); for (const n of geometry.nodes) assert(n.x >= 0 && n.y >= 0 && n.right <= geometry.width + 1 && n.bottom <= geometry.height + 1); assert(geometry.minLabel >= 10, `readable node labels: ${geometry.minLabel}`); for (let i=1; i 700) {assert.equal(a.y,b.y); assert(a.right < b.x);} else {assert.equal(a.x,b.x); assert(a.bottom < b.y);} } // Portal -> diagram and diagram -> portal are both part of the contract. if (width <= 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click(); for (let attempt=0; attempt<3; attempt++) { await page.getByRole('button', {name:/Switch between dark and light mode/}).filter({visible:true}).click(); if (await page.locator('html').getAttribute('data-theme') === 'dark') break; } if (width <= 700) await page.getByRole('button', {name:'Close navigation bar'}).click(); await frame.waitForFunction(() => document.documentElement.dataset.theme === 'dark'); await frame.locator('#btn-theme').click(); await page.waitForFunction(() => document.documentElement.dataset.theme === 'light'); await frame.locator('#btn-theme').click(); await page.waitForFunction(() => document.documentElement.dataset.theme === 'dark'); await checkCards(); await frame.locator('#btn-theme').click(); await page.waitForFunction(() => document.documentElement.dataset.theme === 'light'); for (const preset of ['blueprint','classic']) { await frame.locator('#btn-preset').click(); await frame.locator(`[data-preset-value="${preset}"]`).click(); assert.equal(await frame.locator('html').getAttribute('data-preset'), preset); await checkCards(); } await frame.locator('#btn-export').click(); const [download] = await Promise.all([page.waitForEvent('download'), frame.locator('[data-format="svg"]').click()]); assert.equal(await download.failure(), null); await download.saveAs(`${output}/${width}.svg`); assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('Learn and test')); assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('eldorado-roadmap-typography')); for (const id of ['skills','autonomy']) { await frame.locator(`svg[role="img"] [data-node-id="${id}"]`).screenshot({path:`${output}/${width}-${id}-card.png`}); } const opener = panel.getByRole('button', {name:/^Expand /}); await opener.click(); const close = panel.getByRole('button', {name:'Close full-screen diagram', exact:true}); await close.waitFor({state:'visible'}); assert.equal(await close.innerText(), '×'); await close.click(); assert(await opener.evaluate(n => n === document.activeElement)); await opener.click(); await close.waitFor({state:'visible'}); await page.keyboard.press('Escape'); await page.waitForFunction(() => !document.querySelector('.archify-panel--story dialog').open); assert(await opener.evaluate(n => n === document.activeElement)); assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)); await page.evaluate(() => scrollTo({top:0, behavior:'instant'})); await page.screenshot({path:`${output}/${width}-top.png`}); await headings.first().scrollIntoViewIfNeeded(); await page.screenshot({path:`${output}/${width}-assessment.png`}); await page.screenshot({path:`${output}/${width}-full.png`, fullPage:true}); await page.locator('.strategy-content summary').focus(); await page.keyboard.press('Enter'); await page.waitForFunction(() => document.querySelector('.strategy-content details').open); await page.locator('.strategy-content details p').last().waitFor({state:'visible'}); assert((await page.locator('.strategy-content details').innerText()).includes('exact-artifact approvals')); const links = await page.locator('.strategy-content a').evaluateAll(nodes => nodes.filter(n => !n.classList.contains('hash-link')).map(n => n.href)); for (const href of [...new Set(links)]) { const r = await page.goto(href, {waitUntil:'networkidle'}); assert.equal(r.status(), 200); assert.equal(page.url(), href); } assert.deepEqual(errors, []); results.push({width, geometry, links:links.length, controls:true, export:true, sidebar:true, visibleAssessment:true}); fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2)); await context.close(); } console.log(`PASS ${results.length} Roadmap viewports: three-level story, sidebar, diagram geometry, controls, export and links`); } finally {await browser.close();} })().catch(error => {console.error(error); process.exitCode = 1;});