Files
documentation/scripts/browser/inline-controls.cjs
T
jarvis-at-skic baf79e125b
Build and publish Eldorado documentation / build (pull_request) Successful in 40s
fix: make Target canonical and place Roadmap second
2026-10-05 18:16:57 +00:00

145 lines
9.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');
(async () => {
const base = process.argv[2] || 'http://127.0.0.1:4173';
const output = `.cache/inline-${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 {
for (const width of [1440, 390, 320]) {
const context = await browser.newContext({viewport: {width, height: 1000}, colorScheme: 'light'});
const page = await context.newPage();
await page.goto(`${base}/eldorado-v1/documentation/strategy/`, {waitUntil: 'networkidle'});
const panel = page.locator('.archify-panel--story');
const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile');
await iframe.scrollIntoViewIfNeeded();
const frame = await (await iframe.elementHandle()).contentFrame();
const theme = async value => {
await page.waitForFunction(t => document.documentElement.dataset.theme === t, value);
await frame.waitForFunction(t => document.documentElement.dataset.theme === t, value);
};
await theme('light');
for (const id of ['btn-theme', 'btn-preset', 'btn-export']) {
const box = await frame.locator(`#${id}`).boundingBox();
assert(box && box.width >= 40 && box.height >= 44, id);
}
const toolbar = await frame.locator('.toolbar').boundingBox();
const exportButton = await frame.locator('#btn-export').boundingBox();
const diagram = await frame.locator('.diagram-container > svg').boundingBox();
const rightGap = toolbar.x + toolbar.width - exportButton.x - exportButton.width;
assert(Math.abs(rightGap - 8) < 1, `toolbar right padding at ${width}: ${rightGap}`);
assert(diagram.y >= toolbar.y + toolbar.height, 'toolbar must stay above nodes');
const checkMenu = async selector => {
await frame.locator(selector).evaluate(n => Promise.all(n.getAnimations().map(a => a.finished)));
const menu = await frame.locator(selector).boundingBox();
const viewport = await iframe.boundingBox();
const currentToolbar = await frame.locator('.toolbar').boundingBox();
assert(menu && menu.x >= viewport.x - 1 && menu.y >= currentToolbar.y + currentToolbar.height - 1, selector);
assert(menu.x + menu.width <= viewport.x + viewport.width + 1, `${selector} clips horizontally`);
assert(menu.y + menu.height <= viewport.y + viewport.height + 1, `${selector} clips vertically`);
};
const light = await frame.locator('body').evaluate(n => getComputedStyle(n).backgroundColor);
await frame.locator('#btn-theme').click(); await theme('dark');
assert.notEqual(await frame.locator('body').evaluate(n => getComputedStyle(n).backgroundColor), light);
await frame.locator('#btn-theme').click(); await theme('light');
// Mobile portal theme control lives in the navigation drawer.
const portalTheme = async target => {
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') === target) break;
}
if (width <= 700) await page.getByRole('button', {name: 'Close navigation bar'}).click();
};
await portalTheme('dark'); await theme('dark');
await portalTheme('light'); await theme('light');
for (const preset of ['signal-flow', 'blueprint', 'editorial', 'classic']) {
await frame.locator('#btn-preset').click();
await checkMenu('.preset-menu');
await frame.locator(`[data-preset-value="${preset}"]`).click();
assert.equal(await frame.locator('html').getAttribute('data-preset'), preset);
}
// A theme change must not reload the iframe or lose the selected visual preset.
await frame.locator('#btn-preset').click();
await frame.locator('[data-preset-value="blueprint"]').click();
await portalTheme('dark'); await theme('dark');
assert.equal(await frame.locator('html').getAttribute('data-preset'), 'blueprint');
const exports = [];
for (const format of ['png', 'jpeg', 'webp', 'svg', 'svg-light', 'svg-dark', 'webm']) {
await frame.locator('#btn-export').click();
await checkMenu('.export-menu');
if (format === 'webm' && await frame.locator('[data-format="webm"]').isDisabled()) {
exports.push({format, unsupported: true});
await frame.locator('#btn-export').click();
continue;
}
console.log(`Export ${width} ${format}`);
const [download] = await Promise.all([
page.waitForEvent('download', {timeout: 60000}),
frame.locator(`[data-format="${format}"]`).click(),
]);
assert.equal(await download.failure(), null);
const path = `${output}/${width}-${format}-${download.suggestedFilename()}`;
await download.saveAs(path);
const bytes = fs.readFileSync(path);
assert(bytes.length > 1000);
if (format.startsWith('svg')) {
const valid = await page.evaluate(xml => {
const doc = new DOMParser().parseFromString(xml, 'image/svg+xml');
if (doc.querySelector('[data-node-id="humans"], [data-edge-id="human-direction"]')) return false;
if (![...doc.querySelectorAll('[data-node-id="agent"] text')].some(n => n.textContent === 'Human Controlled')) return false;
if (doc.querySelectorAll('[data-node-id]').length !== 17 || doc.querySelectorAll('path[data-edge-id]').length !== 24) return false;
return !doc.querySelector('parsererror') && doc.documentElement.localName === 'svg' && doc.querySelectorAll('text').length > 0 && doc.querySelector('#eldorado-cycle-export')?.textContent.includes('stroke-dasharray: 3 9');
}, bytes.toString());
assert(valid, format);
} else {
const mime = {png:'image/png', jpeg:'image/jpeg', webp:'image/webp', webm:'video/webm'}[format];
const dimensions = await page.evaluate(async ({data, mime}) => {
const media = document.createElement(mime.startsWith('video') ? 'video' : 'img');
const loaded = new Promise((resolve, reject) => {media[mime.startsWith('video') ? 'onloadedmetadata' : 'onload'] = resolve; media.onerror = reject;});
media.src = `data:${mime};base64,${data}`; await loaded;
return [media.videoWidth || media.naturalWidth, media.videoHeight || media.naturalHeight];
}, {data: bytes.toString('base64'), mime});
assert(dimensions.every(n => n > 0), format);
}
exports.push({format, path, bytes:bytes.length});
}
const opener = panel.getByRole('button', {name:/^Expand /});
await opener.click();
let full = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
await full.waitForFunction(() => document.documentElement.dataset.theme === 'dark');
await full.locator('#btn-theme').click(); await theme('light');
const close = panel.getByRole('button', {name:'Close full-screen diagram'});
assert.equal(await close.innerText(), '×');
await close.click(); assert(await opener.evaluate(n => n === document.activeElement));
await opener.click();
full = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
await full.waitForFunction(() => document.documentElement.dataset.theme === 'light');
await close.focus(); await page.keyboard.press('Escape');
assert(await opener.evaluate(n => n === document.activeElement));
await iframe.scrollIntoViewIfNeeded();
await page.evaluate(() => scrollTo({top: 0, behavior: 'instant'}));
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
await page.screenshot({path:`${output}/${width}.png`, fullPage:true});
await frame.locator('#btn-theme').click(); await theme('dark');
// Exercise keyboard navigation without racing the portal's smooth scroll.
await page.locator('.strategy-content a[href$="/strategy/skills-governance/"]').filter({visible:true}).focus();
await page.keyboard.press('Enter');
await page.waitForURL('**/strategy/skills-governance/');
await page.goBack({waitUntil:'domcontentloaded'});
await page.waitForURL('**/strategy/target-operating-model/');
await page.getByRole('heading', {name:'Target operating model', exact:true, level:1}).waitFor();
const next = page.locator(`.archify-panel--story[data-diagram="strategy-target"] > .story-${width > 700 ? 'desktop' : 'mobile'}`);
await next.scrollIntoViewIfNeeded();
await (await (await next.elementHandle()).contentFrame()).waitForFunction(() => document.documentElement.dataset.theme === 'dark');
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
results.push({width, toolbar, rightGap, dropdownsUnclipped:true, themeSync:true, presets:4, exports, fullscreen:true, routeTheme:true});
fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2));
await context.close();
}
console.log(`PASS ${results.length} viewports: bidirectional themes, 4 presets, all supported exports decoded, fullscreen and route persistence`);
} finally {await browser.close();}
})().catch(error => {console.error(error); process.exitCode = 1;});