273 lines
11 KiB
JavaScript
273 lines
11 KiB
JavaScript
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
|
|
}
|
|
|
|
// Navigate back to a clean campaign view (no modals, no combat)
|
|
async function resetToCleanCampaign(page) {
|
|
const url = page.url();
|
|
await page.goto(url);
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
|
|
// ── captures ───────────────────────────────────────────────────────────────
|
|
|
|
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.
|
|
await resetToCleanCampaign(page);
|
|
}
|
|
|
|
async function captureCharacterSheet(page) {
|
|
// Character cards have an inline border-left-width style unique to them.
|
|
// CSS modules hash class names, so we use the stable inline style instead.
|
|
// Click the top of the card (header/name area) — the middle of the card
|
|
// has a vitalsRow with stopPropagation that would block the sheet from opening.
|
|
const card = page.locator('[style*="border-left-width: 3px"]').first();
|
|
await card.waitFor({ state: 'visible', timeout: 5000 });
|
|
await card.click({ position: { x: 80, y: 18 } }); // top portion = avatar/name row
|
|
await page.waitForTimeout(800);
|
|
// Screenshot is taken here (modal open, stat block visible)
|
|
// Close the modal after so subsequent captures start clean
|
|
}
|
|
|
|
async function captureCharacterCreation(page) {
|
|
// Navigate back to ensure no modals are open (CharacterDetail overlay
|
|
// from the previous capture doesn't respond to Escape — needs a page reload)
|
|
await resetToCleanCampaign(page);
|
|
// Button text is "+ Add Character"
|
|
await page.click('button:has-text("Add Character")');
|
|
await page.waitForTimeout(400);
|
|
// Step 1: fill name (placeholder: "Tharyn the Bold…")
|
|
const nameInput = page.locator('input[placeholder*="Tharyn"]').first();
|
|
await nameInput.fill('Screenshot Hero');
|
|
// Advance to step 2 (Ability Scores — shows the stat rolling UI)
|
|
await page.click('button:has-text("Next")');
|
|
await page.waitForTimeout(400);
|
|
// Screenshot is taken here (stat grid + "Reroll All" button visible)
|
|
// Cancel the wizard after
|
|
}
|
|
|
|
async function captureDiceRoll(page) {
|
|
// Close any open wizard/modal and return to campaign view
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(300);
|
|
await resetToCleanCampaign(page);
|
|
|
|
// Open the first character sheet — click the header area to avoid the
|
|
// vitalsRow which has stopPropagation
|
|
const card = page.locator('[style*="border-left-width: 3px"]').first();
|
|
await card.waitFor({ state: 'visible', timeout: 5000 });
|
|
await card.click({ position: { x: 80, y: 18 } });
|
|
await page.waitForTimeout(800);
|
|
|
|
// Click a stat dice button — title is "Roll 1d20±N (Shift: advantage...)"
|
|
// The stat block may be scrolled below the fold in the modal — use scrollIntoViewIfNeeded
|
|
const statDie = page.locator('button[title^="Roll 1d20"]').first();
|
|
await statDie.waitFor({ state: 'attached', timeout: 5000 });
|
|
await statDie.scrollIntoViewIfNeeded();
|
|
await page.waitForTimeout(200);
|
|
await statDie.click();
|
|
await page.waitForTimeout(2200); // wait for 3D dice animation to settle
|
|
// Screenshot captures the DiceTray overlay + roll log
|
|
}
|
|
|
|
async function captureSpellcasting(page) {
|
|
// Need a Wizard or Priest character to show the spell panel.
|
|
// Re-use one if it already exists (avoids accumulating characters across runs).
|
|
await resetToCleanCampaign(page);
|
|
|
|
let casterCard = page.locator('[style*="border-left-width: 3px"]')
|
|
.filter({ hasText: /Wizard|Priest/ }).first();
|
|
|
|
const hasCaster = await casterCard.isVisible().catch(() => false);
|
|
if (!hasCaster) {
|
|
// Create a Wizard character
|
|
await page.click('button:has-text("Add Character")');
|
|
await page.waitForTimeout(400);
|
|
|
|
const nameInput = page.locator('input[placeholder*="Tharyn"]').first();
|
|
await nameInput.fill('Morgana');
|
|
|
|
// Select Wizard from the Class dropdown (trigger shows "Fighter" by default)
|
|
await page.locator('button:has-text("Fighter")').first().click();
|
|
await page.waitForTimeout(200);
|
|
await page.locator('button:has-text("Wizard")').last().click();
|
|
await page.waitForTimeout(200);
|
|
|
|
// Step 1 → Step 2 (stats)
|
|
await page.click('button:has-text("Next")');
|
|
await page.waitForTimeout(300);
|
|
// Step 2 → Step 3 (background)
|
|
await page.click('button:has-text("Next")');
|
|
await page.waitForTimeout(300);
|
|
// Pick a random background and advance
|
|
await page.click('button:has-text("Random Background")');
|
|
await page.waitForTimeout(200);
|
|
await page.click('button:has-text("Next")');
|
|
await page.waitForTimeout(300);
|
|
// Create the character
|
|
await page.click('button:has-text("Create Character")');
|
|
await page.waitForTimeout(1200);
|
|
|
|
// Re-resolve the locator after creation
|
|
casterCard = page.locator('[style*="border-left-width: 3px"]')
|
|
.filter({ hasText: /Wizard|Priest/ }).first();
|
|
}
|
|
|
|
await casterCard.waitFor({ state: 'visible', timeout: 5000 });
|
|
await casterCard.click({ position: { x: 80, y: 18 } });
|
|
await page.waitForTimeout(600);
|
|
|
|
// Enter edit mode to add a spell
|
|
await page.click('button:has-text("Edit")');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Open the spell picker
|
|
await page.click('button:has-text("Add Spell")');
|
|
await page.waitForTimeout(500); // wait for spell list to load
|
|
|
|
// Pick the first available spell
|
|
const firstSpell = page.locator('[class*="pickerItem"]').first();
|
|
await firstSpell.waitFor({ state: 'visible', timeout: 5000 });
|
|
await firstSpell.click();
|
|
await page.waitForTimeout(300);
|
|
|
|
// Exit edit mode
|
|
await page.click('button:has-text("Done")');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Cast the spell to get an exhausted state (shows the exhausted indicator)
|
|
const castBtn = page.locator('button:has-text("Cast")').first();
|
|
await castBtn.waitFor({ state: 'visible', timeout: 3000 });
|
|
await castBtn.click();
|
|
await page.waitForTimeout(800);
|
|
|
|
// Close any cast result modal
|
|
const resultClose = page.locator('button:has-text("Close"), button:has-text("OK")').first();
|
|
if (await resultClose.isVisible().catch(() => false)) {
|
|
await resultClose.click();
|
|
await page.waitForTimeout(200);
|
|
}
|
|
// Screenshot captures the spell list with slot/exhausted state visible
|
|
}
|
|
|
|
async function captureAtmosphere(page) {
|
|
// Navigate back to ensure the campaign view is accessible (no open modals)
|
|
await resetToCleanCampaign(page);
|
|
// Open the atmosphere panel — trigger button has title="Atmosphere effects"
|
|
await page.click('button[title="Atmosphere effects"]');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Toggle Fog on — the toggle button has aria-label="Toggle Fog"
|
|
await page.click('button[aria-label="Toggle Fog"]');
|
|
await page.waitForTimeout(800); // wait for fog to render
|
|
// Screenshot captures fog effect over the campaign view
|
|
|
|
// Close the atmosphere panel after so it doesn't clutter other captures
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
async function captureInitiativeActive(page) {
|
|
await resetToCleanCampaign(page);
|
|
|
|
// Open combat start modal — button text is "⚔ Combat"
|
|
await page.click('button:has-text("Combat")');
|
|
await page.waitForTimeout(400);
|
|
|
|
// Add an enemy so the Roll Initiative button is fully useful
|
|
// (characters are pre-selected by default, so canStart is already true)
|
|
const enemyNameInput = page.locator('input[placeholder*="Goblin"], input[placeholder*="enemy" i], input[placeholder*="name" i]').first();
|
|
if (await enemyNameInput.isVisible().catch(() => false)) {
|
|
await enemyNameInput.fill('Goblin');
|
|
const hpInput = page.locator('input[placeholder*="HP"], input[placeholder*="hp" i], input[type="number"]').first();
|
|
if (await hpInput.isVisible().catch(() => false)) {
|
|
await hpInput.fill('5');
|
|
}
|
|
}
|
|
|
|
// Click "Roll Initiative ⚔" to start combat
|
|
await page.click('button:has-text("Roll Initiative")');
|
|
await page.waitForTimeout(1200); // wait for initiative state to sync via socket
|
|
|
|
// Screenshot captures the initiative tracker with party + enemies rolled
|
|
}
|
|
|
|
// ── 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);
|
|
});
|