feat: add brochure screenshots and fix Playwright capture selectors

This commit is contained in:
Aaron Wood 2026-04-11 19:52:12 -04:00
parent 4b46be9e1b
commit 3059fbaedf
8 changed files with 184 additions and 58 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,018 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

View file

@ -42,69 +42,195 @@ async function openCampaign(page) {
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 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
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 ───────────────────────────────────────────────────────────────────