feat: add brochure screenshots and fix Playwright capture selectors
BIN
site/assets/screenshots/atmosphere.png
Normal file
|
After Width: | Height: | Size: 1,018 KiB |
BIN
site/assets/screenshots/character-creation.png
Normal file
|
After Width: | Height: | Size: 621 KiB |
BIN
site/assets/screenshots/character-sheet.png
Normal file
|
After Width: | Height: | Size: 764 KiB |
BIN
site/assets/screenshots/dice-roll.png
Normal file
|
After Width: | Height: | Size: 765 KiB |
BIN
site/assets/screenshots/dm-cards.png
Normal file
|
After Width: | Height: | Size: 883 KiB |
BIN
site/assets/screenshots/initiative-active.png
Normal file
|
After Width: | Height: | Size: 877 KiB |
BIN
site/assets/screenshots/spellcasting.png
Normal file
|
After Width: | Height: | Size: 507 KiB |
|
|
@ -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 ───────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||