Source code
src/scripts/presentation/capture.mjs
This file is included in this documentation build. It is displayed as code and is not executed.
/**
* Refresh real showcase screenshots in three languages from an explicitly selected local server.
*
* @author Laurent Declercq l.declercq@agon-innovation.ch
* @version 20260921
*/
import fs from 'node:fs/promises';
import path from 'node:path';
import { parseArgs } from 'node:util';
import { chromium } from 'playwright-core';
const { values: args } = parseArgs({
options: {
url: { type: 'string', default: 'http://127.0.0.1:4173/' },
locale: { type: 'string' },
output: { type: 'string' },
help: { type: 'boolean' }
}
});
if (args.help) {
console.log('Usage: yarn presentation:capture [--url http://127.0.0.1:4173/] [--locale en|de|fr] [--output DIRECTORY]. Start the showcase separately.');
process.exit(0);
}
const root = path.resolve(import.meta.dirname, '../../..');
const output = path.resolve(args.output || path.join(root, 'docs/presentation/source/screenshots'));
const locales = args.locale
? [ args.locale ]
: [ 'en', 'de', 'fr' ];
for (const locale of locales) {
if (![ 'en', 'de', 'fr' ].includes(locale)) {
throw new Error('Unknown presentation language.');
}
}
const base = new URL(args.url);
if (![ 'http:', 'https:' ].includes(base.protocol)) {
throw new Error('The showcase URL must use HTTP or HTTPS.');
}
if (!base.pathname.endsWith('/')) {
base.pathname += '/';
}
await fs.mkdir(output, { recursive: true });
const browser = await chromium.launch();
try {
for (const locale of locales) {
const messages = {
...JSON.parse(await fs.readFile(path.join(root, `src/atrium/locales/${locale}.json`))),
...JSON.parse(await fs.readFile(path.join(root, `src/showcase/locales/${locale}.json`)))
};
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
reducedMotion: 'reduce'
});
await context.addInitScript(
/**
* Select the capture language before the isolated showcase context starts.
*
* @param {string} language - Interface language.
* @returns {void} Stores this context's appearance preference.
*/
(language) => localStorage.setItem('agon-showcase:v1:appearance', JSON.stringify({
locale: language,
theme: 'dark'
})),
locale
);
const page = await context.newPage();
/**
* Navigate relative to the configured application base and wait for its visible heading.
*
* @param {string} route - Application route.
* @returns {Promise<void>} Resolves once the page has mounted.
*/
async function go(route) {
await page.goto(new URL(route, base).href);
await page.locator('#page-content h1:visible').first().waitFor();
await page.waitForTimeout(500);
}
/**
* Save a settled viewport or component screenshot in the selected source directory.
*
* @param {string} name - Stable screenshot name.
* @param {string} [selector] - Optional component selector.
* @returns {Promise<void>} Resolves after writing the PNG.
*/
async function snap(name, selector) {
await page.mouse.move(0, 0);
await page.waitForTimeout(250);
await (selector ? page.locator(selector) : page).screenshot({
path: path.join(output, `${locale}-${name}.png`)
});
console.log('Captured', locale, name);
}
await go('');
await snap('dashboard');
await go('components/chart');
await page.locator('.apexcharts-canvas').first().waitFor();
await page.waitForTimeout(600);
await snap('chart', '[data-chart-example="remote"]');
await snap('chart-donut', '[data-chart-example="donut"]');
await go('resources');
await snap('resources');
await go('forms');
await snap('forms');
await go('components/dialog');
await page.getByRole('button', { name: messages.openDraggableDialog, exact: true }).click();
await snap('dialog');
await go('components/drawer');
await page.getByRole('button', { name: messages.galleryDrawer, exact: true }).click();
await snap('drawer');
await go('components/buttons');
await snap('buttons', '#page-content .catalog-grid');
await go('components/icons');
await page.locator('.icon-tile').first().waitFor();
await snap('icons');
await go('');
await page.getByRole('button', { name: messages.customizeTheme, exact: true }).click();
await page.getByRole('button', { name: messages.light, exact: true }).click();
await snap('theme');
await go('components/map');
await page.getByRole('button', { name: messages.mapLoad, exact: true }).click();
await page.locator('.leaflet-container').waitFor();
await snap('map');
await go('components/terminal');
await page.getByRole('button', { name: messages.connect, exact: true }).click();
await page.locator('.xterm-helper-textarea').pressSequentially('whoami');
await page.locator('.xterm-helper-textarea').press('Enter');
await page.locator('.xterm-helper-textarea').pressSequentially('help');
await page.locator('.xterm-helper-textarea').press('Enter');
await page.getByRole('status').filter({ hasText: messages.surface_connected }).waitFor();
await snap('terminal', '.ui-console');
await go('components/vnc');
await page.getByRole('button', { name: messages.connect, exact: true }).click();
await page.locator('.ui-vnc canvas[width="640"]').waitFor();
await snap('vnc', '.ui-console');
await go('components');
await snap('catalogue');
await go('login');
await snap('login');
await page.setViewportSize({ width: 430, height: 860 });
await go('');
await snap('mobile');
await context.close();
}
} finally {
await browser.close();
}
console.log('Screenshot capture complete. Regenerate slides before rebuilding videos.');