diff --git a/AGENTS.md b/AGENTS.md index 272ac328..0b6ef049 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -267,3 +267,5 @@ test('example test', async ({ page }) => { ## Documentation When changing behaviour that is documented, update the related docs too: `README.md` (configuration, feature flags), `docs/`, `.env.example` and the Helm chart values. + +Screenshots in the Antora docs (`docs/modules/cockpit/images/`) are generated by the specs in `e2e/docs-screenshots/`. Regenerate them with `npm run docs:screenshots` after changing the UI they show. Add new screenshots as specs there, never by hand. diff --git a/README.md b/README.md index b3dff441..70ae1ab7 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ npm run knip # Report unused files, exports and dependencies npm run test:unit # Run the Vitest unit tests npm run test:arch # Run the architecture tests npm run test:e2e # Run the Playwright end-to-end tests (requires Docker) +npm run docs:screenshots # Regenerate the screenshots in the Antora docs (requires Docker) npm run generate:antlr # Regenerate the lexer/parser after editing the SQL grammar ``` diff --git a/docs/modules/cockpit/images/dashboard.png b/docs/modules/cockpit/images/dashboard.png index a97178e8..f349afd8 100644 Binary files a/docs/modules/cockpit/images/dashboard.png and b/docs/modules/cockpit/images/dashboard.png differ diff --git a/e2e/docs-screenshots/dashboard.spec.ts b/e2e/docs-screenshots/dashboard.spec.ts new file mode 100644 index 00000000..18e8d9dd --- /dev/null +++ b/e2e/docs-screenshots/dashboard.spec.ts @@ -0,0 +1,60 @@ +import path from 'path'; +import { test, expect } from '../support/fixtures.js'; +import { waitForHydration } from '../support/helpers'; +import { resetBookmarks } from '../support/bookmarks.js'; + +const imagesDir = path.join(import.meta.dirname, '../../docs/modules/cockpit/images'); + +// The global bookmarks from .env.test ("Cluster Logs", "Team Wiki") show up as well. +test('dashboard with bookmarks', async ({ page }) => { + await resetBookmarks(page, [ + { + productId: 'trino', + name: 'Trino', + url: 'https://trino.example.com', + environment: 'production', + pinned: true + }, + { + productId: 'superset', + name: 'Sales dashboards', + url: 'https://superset.example.com', + environment: 'production', + pinned: true + }, + { + productId: 'airflow', + name: 'Airflow', + url: 'https://airflow.example.com', + environment: 'production' + }, + { + productId: 'nifi', + name: 'Ingestion', + url: 'https://nifi.example.com', + environment: 'staging' + }, + { + productId: 'spark', + name: 'Spark History', + url: 'https://spark-history.example.com', + environment: 'production' + }, + { + productId: 'druid', + name: 'Druid', + url: 'https://druid.example.com', + environment: 'production' + } + ]); + try { + await page.goto('/'); + await waitForHydration(page); + // Product logos load lazily. + await page.waitForLoadState('networkidle'); + await expect(page.getByLabel('Bookmarks').getByText('Sales dashboards').first()).toBeVisible(); + await page.screenshot({ path: path.join(imagesDir, 'dashboard.png') }); + } finally { + await resetBookmarks(page); + } +}); diff --git a/e2e/support/containers/playwright-browser.setup.ts b/e2e/support/containers/playwright-browser.setup.ts new file mode 100644 index 00000000..754bf9ce --- /dev/null +++ b/e2e/support/containers/playwright-browser.setup.ts @@ -0,0 +1,37 @@ +import { createRequire } from 'node:module'; +import { GenericContainer, type StartedTestContainer, Wait } from 'testcontainers'; + +// Fixed so that playwright.config.ts can point the docs project at it. +export const PLAYWRIGHT_BROWSER_SERVER_PORT = 19300; + +/** + * Starts a Playwright browser server in the official Playwright image, so the + * docs screenshots use the same browser build and fonts on every machine. + * Client and server must run the same Playwright version. + */ +export async function startPlaywrightBrowserServer(): Promise { + const { version } = createRequire(import.meta.url)('@playwright/test/package.json') as { + version: string; + }; + const start = Date.now(); + console.log(`Starting Playwright ${version} browser server testcontainer...`); + const container = await new GenericContainer(`mcr.microsoft.com/playwright:v${version}-noble`) + .withUser('pwuser') + .withWorkingDir('/home/pwuser') + .withCommand([ + 'npx', + '-y', + `playwright@${version}`, + 'run-server', + '--port', + '3000', + '--host', + '0.0.0.0' + ]) + .withExposedPorts({ container: 3000, host: PLAYWRIGHT_BROWSER_SERVER_PORT }) + .withStartupTimeout(300_000) + .withWaitStrategy(Wait.forLogMessage(/Listening on/)) + .start(); + console.log(`Playwright browser server started in ${Date.now() - start}ms`); + return container; +} diff --git a/e2e/support/start-app.ts b/e2e/support/start-app.ts index 95a6badf..5e82d59f 100644 --- a/e2e/support/start-app.ts +++ b/e2e/support/start-app.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import type { StartedTestContainer } from 'testcontainers'; import { startPostgres } from './containers/postgres.setup.js'; import { startGarage } from './containers/garage.setup.js'; +import { startPlaywrightBrowserServer } from './containers/playwright-browser.setup.js'; import { productionBodySizeLimit } from './body-size-limit.js'; process.loadEnvFile(path.join(import.meta.dirname, '../..', '.env.test')); @@ -32,6 +33,9 @@ async function main() { // and npm run test:e2e use the same startup order and automatic teardown. containers.push(await startPostgres()); containers.push(await startGarage()); + if (process.env.DOCS_SCREENSHOTS) { + containers.push(await startPlaywrightBrowserServer()); + } const testEnv = Object.fromEntries( Object.entries(process.env).filter( diff --git a/package.json b/package.json index 3bf8d821..535bba22 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "test:unit:coverage": "vitest run --coverage", "test:e2e": "node --env-file=.env.test node_modules/.bin/vite build && playwright test", "test:e2e:ui": "playwright test --ui", + "docs:screenshots": "node --env-file=.env.test node_modules/.bin/vite build && DOCS_SCREENSHOTS=1 playwright test --project docs", "generate:antlr": "cd src/lib/editor/grammar && antlr-ng -v -l -Dlanguage=TypeScript -o ../generated SqlBase.g4 && node patch-generated.mjs", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", diff --git a/playwright.config.ts b/playwright.config.ts index 6839f9e4..ad590402 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,10 +1,33 @@ import { defineConfig } from '@playwright/test'; import path from 'path'; +import { PLAYWRIGHT_BROWSER_SERVER_PORT } from './e2e/support/containers/playwright-browser.setup.js'; import { APP_VARIANTS } from './e2e/support/app-variants.js'; const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:4173'; const appPort = new URL(baseURL).port || '80'; const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; +// The specs that write the screenshots of the Antora docs run only in the docs +// project, which `npm run docs:screenshots` enables, not in the normal E2E run. +const docsScreenshotSpecs = '**/e2e/docs-screenshots/*.spec.ts'; +const docsProject = { + name: 'docs', + testMatch: docsScreenshotSpecs, + retries: 0, + use: { + browserName: 'chromium' as const, + viewport: { width: 1280, height: 800 }, + colorScheme: 'light' as const, + locale: 'en-US', + // The browser runs in the official Playwright image (started by + // e2e/support/start-app.ts), so the screenshots don't depend on the + // browser build and fonts of the machine. It reaches the app on the + // host's loopback interface through the Playwright connection. + connectOptions: { + wsEndpoint: `ws://127.0.0.1:${PLAYWRIGHT_BROWSER_SERVER_PORT}/`, + exposeNetwork: '' + } + } +}; const browsers = { firefox: { browserName: 'firefox' }, chromium: { @@ -74,7 +97,7 @@ export default defineConfig({ projects: [ ...Object.entries(browsers).map(([name, browser]) => ({ name, - testIgnore: variantSpecs, + testIgnore: [...variantSpecs, docsScreenshotSpecs], use: { ...browser, viewport: { width: 1280, height: 720 } } })), ...variants.flatMap((variant) => @@ -83,6 +106,7 @@ export default defineConfig({ testMatch: variant.specs, use: { ...browser, baseURL: variant.baseURL, viewport: { width: 1280, height: 720 } } })) - ) + ), + ...(process.env.DOCS_SCREENSHOTS ? [docsProject] : []) ] });