Files
pulse/scripts/release_control/capture_release_note_visuals.mjs
T
2026-08-28 11:29:44 +01:00

120 lines
3.8 KiB
JavaScript
Executable File

#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { chromium } from '@playwright/test';
const [, , planPath, beforeBaseURL, afterBaseURL, outputDirectory] = process.argv;
if (!planPath || !beforeBaseURL || !afterBaseURL || !outputDirectory) {
throw new Error(
'usage: capture_release_note_visuals.mjs PLAN BEFORE_BASE_URL AFTER_BASE_URL OUTPUT_DIRECTORY',
);
}
const plan = JSON.parse(await fs.readFile(planPath, 'utf8'));
await fs.mkdir(outputDirectory, { recursive: true });
const username = process.env.PULSE_RELEASE_VISUAL_USERNAME || 'admin';
const password = process.env.PULSE_RELEASE_VISUAL_PASSWORD || 'adminadminadmin';
function sameOriginURL(route, baseURL) {
const base = new URL(baseURL);
const destination = new URL(route, base);
if (destination.origin !== base.origin) {
throw new Error(`capture route escaped the application origin: ${route}`);
}
return destination.toString();
}
function locatorFor(page, descriptor) {
let locator;
switch (descriptor.kind) {
case 'role':
locator = page.getByRole(descriptor.role, {
name: descriptor.name,
exact: descriptor.exact,
});
break;
case 'text':
locator = page.getByText(descriptor.value, { exact: descriptor.exact });
break;
case 'label':
locator = page.getByLabel(descriptor.value, { exact: descriptor.exact });
break;
case 'testid':
locator = page.getByTestId(descriptor.value);
break;
default:
throw new Error(`unsupported locator kind: ${descriptor.kind}`);
}
return locator.nth(descriptor.nth || 0);
}
async function authenticate(page, baseURL) {
await page.goto(new URL('/', baseURL).toString(), { waitUntil: 'domcontentloaded' });
const usernameInput = page.locator('input[name="username"]');
if (await usernameInput.isVisible({ timeout: 15_000 }).catch(() => false)) {
await usernameInput.fill(username);
await page.locator('input[name="password"]').fill(password);
await page.locator('button[type="submit"]').click();
}
await page
.locator('input[name="username"]')
.waitFor({ state: 'hidden', timeout: 30_000 });
}
async function captureState(browser, baseURL, capture, state, suffix) {
const context = await browser.newContext({
viewport: capture.viewport,
colorScheme: 'dark',
reducedMotion: 'reduce',
locale: 'en-GB',
timezoneId: 'UTC',
});
const page = await context.newPage();
try {
await authenticate(page, baseURL);
const response = await page.goto(sameOriginURL(state.route, baseURL), {
waitUntil: 'domcontentloaded',
});
if (!response || !response.ok()) {
throw new Error(`capture route did not load successfully: ${state.route}`);
}
for (const step of state.steps || []) {
const locator = locatorFor(page, step.locator);
if (step.action === 'click') {
await locator.click({ timeout: 15_000 });
} else {
await locator.waitFor({ state: 'visible', timeout: 15_000 });
}
}
await locatorFor(page, state.ready).waitFor({ state: 'visible', timeout: 20_000 });
await page.waitForTimeout(750);
const outputPath = path.join(
outputDirectory,
`release-note-${capture.id}-${suffix}.png`,
);
await page.screenshot({
path: outputPath,
fullPage: false,
animations: 'disabled',
caret: 'hide',
});
} finally {
await context.close();
}
}
const browser = await chromium.launch({ headless: true });
try {
for (const capture of plan.captures) {
if (capture.before) {
await captureState(browser, beforeBaseURL, capture, capture.before, 'before');
}
await captureState(browser, afterBaseURL, capture, capture.after, 'now');
}
} finally {
await browser.close();
}