/** * 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} 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} 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.');