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'); return !doc.querySelector('parsererror') && doc.documentElement.localName === 'svg' && doc.querySelectorAll('text').length > 0; }, 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(0, 0)); await page.screenshot({path:`${output}/${width}.png`, fullPage:true}); await frame.locator('#btn-theme').click(); await theme('dark'); await page.locator('.strategy-content a[href$="/strategy/target-operating-model/"]').filter({visible:true}).click(); 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;});