feat: add Playwright screenshot script for brochure

Adds site/package.json and site/screenshots.js to automate capturing
7 named screenshots of the running Darkwatch app via Playwright/Chromium.
Also adds site/node_modules/ to .gitignore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Wood 2026-04-11 18:34:07 -04:00
parent c11d6721f9
commit e1cf22cae4
4 changed files with 219 additions and 0 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ server/data/
server/.env
*.db
.claude/settings.local.json
site/node_modules/

60
site/package-lock.json generated Normal file
View file

@ -0,0 +1,60 @@
{
"name": "darkwatch-brochure-screenshots",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "darkwatch-brochure-screenshots",
"devDependencies": {
"playwright": "^1.43.0"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

11
site/package.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "darkwatch-brochure-screenshots",
"type": "module",
"scripts": {
"screenshots": "node screenshots.js",
"screenshots:only": "node screenshots.js --only"
},
"devDependencies": {
"playwright": "^1.43.0"
}
}

147
site/screenshots.js Normal file
View file

@ -0,0 +1,147 @@
import { chromium } from 'playwright';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { mkdirSync } from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SHOT_DIR = resolve(__dirname, 'assets', 'screenshots');
const APP = 'http://localhost:5173';
const DM_EMAIL = 'dm@darkwatch.test';
const DM_PASS = 'password';
const ONLY = process.argv.includes('--only')
? process.argv[process.argv.indexOf('--only') + 1]
: null;
mkdirSync(SHOT_DIR, { recursive: true });
// ── helpers ────────────────────────────────────────────────────────────────
async function shot(page, name, fn) {
if (ONLY && name !== ONLY) return;
console.log(` capturing ${name}...`);
await fn(page);
await page.screenshot({
path: resolve(SHOT_DIR, `${name}.png`),
fullPage: false,
});
console.log(`${name}.png`);
}
async function login(page) {
await page.goto(`${APP}/login`);
await page.fill('input[type="email"]', DM_EMAIL);
await page.fill('input[type="password"]', DM_PASS);
await page.click('button[type="submit"]');
await page.waitForURL(`${APP}/`);
}
async function openCampaign(page) {
// Seeded campaign is named "Tomb of the Serpent King"
await page.click('text=Tomb of the Serpent King');
await page.waitForURL(/\/campaign\/\d+/);
await page.waitForTimeout(1500); // socket connect + data load
}
// ── captures ───────────────────────────────────────────────────────────────
async function captureCharacterSheet(page) {
// DM view — click the first character card to open full sheet
// Selector: first .characterCard element in the card grid
// Adjust class name if different in the actual DOM
const card = page.locator('[class*="characterCard"]').first();
await card.click();
await page.waitForTimeout(500);
}
async function captureCharacterCreation(page) {
// Open character creation wizard and advance to step 2 (stat rolling)
await page.click('button:has-text("New Character")');
await page.waitForTimeout(400);
// Fill required fields on step 1 so Next is enabled
const nameInput = page.locator('input[placeholder*="name" i]').first();
await nameInput.fill('Screenshot Hero');
// Advance to step 2
await page.click('button:has-text("Next")');
await page.waitForTimeout(400);
}
async function captureDiceRoll(page) {
// Roll a d20 — find the d20 button in the dice panel
await page.click('button:has-text("d20")');
await page.waitForTimeout(2000); // wait for 3D animation to settle
}
async function captureSpellcasting(page) {
// Open the Spells tab/panel for the first Wizard or Priest character
// The spell panel may be a tab within the character sheet
await page.click('text=Spells');
await page.waitForTimeout(400);
}
async function captureAtmosphere(page) {
// Open atmosphere panel and enable fog
await page.click('button[title*="tmosphere" i], button:has-text("Atmosphere")');
await page.waitForTimeout(300);
await page.click('button:has-text("Fog")');
await page.waitForTimeout(600); // wait for fog to render
}
async function captureInitiativeActive(page) {
// Click the Combat button to open the start combat modal
await page.click('button:has-text("Combat")');
await page.waitForTimeout(300);
// Start combat with whatever defaults are present
await page.click('button:has-text("Start Combat")');
await page.waitForTimeout(500);
// Roll initiative for enemies (DM roll)
const rollBtn = page.locator('button:has-text("Roll")').first();
await rollBtn.click();
await page.waitForTimeout(400);
}
async function captureDmCards(page) {
// The DM compact card grid is the default campaign view.
// Navigate back to campaign root to ensure no modals are open.
const url = page.url();
await page.goto(url);
await page.waitForTimeout(1500); // let socket reconnect
}
// ── main ───────────────────────────────────────────────────────────────────
async function run() {
console.log('Starting Darkwatch screenshot capture...');
console.log(`App: ${APP}`);
if (ONLY) console.log(`Only: ${ONLY}`);
console.log('');
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
});
const page = await context.newPage();
await login(page);
console.log('✓ Logged in as DM');
await openCampaign(page);
console.log('✓ Campaign open');
console.log('');
await shot(page, 'dm-cards', captureDmCards);
await shot(page, 'character-sheet', captureCharacterSheet);
await shot(page, 'character-creation',captureCharacterCreation);
await shot(page, 'dice-roll', captureDiceRoll);
await shot(page, 'spellcasting', captureSpellcasting);
await shot(page, 'atmosphere', captureAtmosphere);
await shot(page, 'initiative-active', captureInitiativeActive);
await browser.close();
console.log('');
console.log('Done! Screenshots saved to site/assets/screenshots/');
}
run().catch(err => {
console.error(err);
process.exit(1);
});