Build and publish Eldorado documentation / build (pull_request) Successful in 41s
140 lines
9.1 KiB
JavaScript
140 lines
9.1 KiB
JavaScript
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 && 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/target-operating-model/"]').filter({visible:true}).focus();
|
||
await page.keyboard.press('Enter');
|
||
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;});
|