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:
parent
c11d6721f9
commit
e1cf22cae4
4 changed files with 219 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@ server/data/
|
||||||
server/.env
|
server/.env
|
||||||
*.db
|
*.db
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
site/node_modules/
|
||||||
|
|
|
||||||
60
site/package-lock.json
generated
Normal file
60
site/package-lock.json
generated
Normal 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
11
site/package.json
Normal 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
147
site/screenshots.js
Normal 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);
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue