Files
documentation/scripts/browser/roadmap-timeline.cjs
T
jarvis-at-skic 2a4e058684
Build and publish Eldorado documentation / build (pull_request) Successful in 54s
fix: [verified] simplify Roadmap adoption timeline
2026-10-06 07:25:53 +00:00

130 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const {chromium} = require('playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const ids = ['assessment','readiness','pilot','expansion','sustain'];
(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, vertical) => {
await frame.locator('svg[role="img"]').waitFor();
const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({
width:innerWidth, height:innerHeight,
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,
numberColor:getComputedStyle(node.querySelector('[data-stage-number]')).fill,
titleColor:getComputedStyle(node.querySelector('[data-node-label]')).fill,
text:[...node.querySelectorAll('text')].map(text => ({value:text.textContent, box:text.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(text).fontSize)*text.getScreenCTM().a}))
}))
}));
geometry.cards.sort((a,b) => a.box.y-b.box.y || a.box.x-b.box.x);
assert.deepEqual(geometry.cards.map(n => n.id), ids);
assert.deepEqual(geometry.cards.map(n => n.number), ['1','2','3','4','5']);
for (const [index, card] of geometry.cards.entries()) {
assert.equal(card.text.length, 3, 'number, title and one outcome only');
assert.equal(card.numberColor,card.titleColor,'stage number must inherit readable theme color');
assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width + 1 && card.box.bottom <= geometry.height + 1, `outside iframe: ${card.id}`);
for (const text of card.text) {
assert(text.size >= 13, `small text: ${card.id} ${text.size}`);
assert(!/OUTPUT|Exit:/.test(text.value));
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: ${card.id}`);
}
for (let t=1; t<card.text.length; t++) assert(card.text[t-1].box.bottom < card.text[t].box.top, `overlapping text: ${card.id}`);
if (index) {
const previous = geometry.cards[index-1].box;
if (vertical) {assert.equal(previous.x,card.box.x); assert(previous.bottom < card.box.top);}
else {assert.equal(previous.y,card.box.y); assert(previous.right < card.box.left);}
}
}
return geometry;
};
try {
for (const width of [1440,1024,390,320]) {
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));
assert.equal((await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'})).status(),200);
const panel = page.locator('[data-diagram="strategy-roadmap"]');
const iframe = panel.locator(width < 1360 ? '.story-mobile' : '.story-desktop');
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']) {
if (await frame.locator('html').getAttribute('data-theme') !== theme) await frame.locator('#btn-theme').click();
await page.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
const inline = await inspect(frame,width < 1360);
await panel.screenshot({path:`${output}/${width}-${theme}-inline.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 fullscreen = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
await fullscreen.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
const expanded = await inspect(fullscreen,width < 1000);
await page.screenshot({path:`${output}/${width}-${theme}-fullscreen.png`});
await page.keyboard.press('Escape');
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
assert(await opener.evaluate(n => n === document.activeElement));
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
results.push({width,theme,inline,expanded});
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
}
assert.deepEqual(errors,[]);
} finally {await context.close();}
}
assert.equal(results.length,8);
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'});
const panel = page.locator('[data-diagram="strategy-roadmap"]');
const opener = panel.getByRole('button', {name:/^Expand /});
await opener.focus(); await page.keyboard.press('Enter');
const modal = panel.locator('dialog');
const close = modal.getByRole('button', {name:'Close full-screen diagram', exact:true});
await close.waitFor({state:'visible'});
const iframe = modal.locator('iframe');
const frame = await (await iframe.elementHandle()).contentFrame();
const geometry = await inspect(frame,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 frameBox = await iframe.boundingBox();
const toolbarBox = await modal.locator('.archify-modal-toolbar').boundingBox();
const closeBox = await close.boundingBox();
assert(closeBox.y >= 0 && closeBox.y + closeBox.height <= viewport.height, 'close remains visible');
assert(frameBox.y + card.box.top >= toolbarBox.y + toolbarBox.height - 1 && frameBox.y + card.box.bottom <= viewport.height + 1, `stage unreachable: ${card.id}`);
}
// Controls stay reachable at the beginning of the scrollable viewer.
await modal.evaluate(n => {n.scrollTop = 0;});
for (const selector of ['#btn-theme','#btn-preset','#btn-export']) {
const control = frame.locator(selector);
const box = await control.boundingBox();
assert(box.x >= 0 && box.x + box.width <= viewport.width && box.y >= 0 && box.y + box.height <= viewport.height, `control outside viewport: ${selector}`);
await control.click();
if (selector !== '#btn-theme') await control.click();
}
await close.focus(); await page.keyboard.press('Escape');
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
await iframe.waitFor({state:'detached'});
assert(await opener.evaluate(n => n === document.activeElement), 'Escape restores opener focus');
await page.keyboard.press('Enter');
await close.waitFor({state:'visible'});
await close.focus(); await page.keyboard.press('Enter');
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
assert(await opener.evaluate(n => n === document.activeElement), 'close button restores opener focus');
results.push({viewport,scrollable:geometry});
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
} finally {await context.close();}
}
assert.equal(results.length,10);
console.log('PASS 16 timeline contexts: 4 widths × 2 themes × inline/fullscreen; ordered readable text, no clipping, keyboard exit.');
console.log('PASS short fullscreen 390×667 and 320×568: readable stages reachable by scroll, sticky close, accessible controls, keyboard close and focus restoration.');
} finally {await browser.close();}
})().catch(error => {console.error(error); process.exitCode=1;});