Skip to content

Source code

Source code

src/scripts/presentation/capture.mjs

This file is included in this documentation build. It is displayed as code and is not executed.

Download source

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