const { test, expect } = require("@playwright/test"); async function postJson(request, url, data) { const response = await request.post(url, { data }); expect(response.ok()).toBeTruthy(); return await response.json(); } async function registerAndLogin(request, username, displayName) { await postJson(request, "/api/auth/register", { username, password: "Password123", displayName }); const loginResponse = await request.post("/api/auth/login", { data: { username, password: "Password123" } }); expect(loginResponse.ok()).toBeTruthy(); } test("home page loads auth entry points", async ({ page }) => { await page.goto("/"); await expect(page.locator("h1")).toContainText("RpgRoller"); await expect(page.getByRole("heading", { name: "Register" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Login" })).toBeVisible(); await expect(page.getByLabel("Username").first()).toBeVisible(); await expect(page.getByLabel("Password").nth(1)).toBeVisible(); }); test("Rolemaster open-ended roll detail renders specialized dice chips", async ({ page, context }) => { const username = `rm-${Date.now()}`; const displayName = "Rolemaster Smoke"; await registerAndLogin(context.request, username, displayName); const campaign = await postJson(context.request, "/api/campaigns", { name: "Rolemaster Smoke", rulesetId: "rolemaster" }); const character = await postJson(context.request, "/api/characters", { name: "Open Ender", campaignId: campaign.id }); const skill = await postJson(context.request, `/api/characters/${character.id}/skills`, { name: "Open Sight", diceRollDefinition: "d100!+85", wildDice: 0, allowFumble: false, fumbleRange: 95 }); await postJson(context.request, `/api/skills/${skill.id}/roll`, { visibility: "public" }); await page.goto("/"); await expect(page.getByText("Campaign Log")).toBeVisible(); await expect(page.locator(".log-panel .log-event-badge")).toHaveText("Fumble"); const logEntry = page.locator(".log-panel .log-entry-toggle").first(); await expect(logEntry).toBeVisible(); await logEntry.click(); const rolemasterFollowUpDice = page.locator(".die-chip.rolemaster-open-ended-high, .die-chip.rolemaster-open-ended-low-subtract"); await expect(rolemasterFollowUpDice.first()).toBeVisible(); await expect(page.locator(".log-detail .roll-dice-strip")).toBeVisible(); }); test("newly rolled log entry auto-expands", async ({ page, context }) => { const username = `d6-log-${Date.now()}`; await registerAndLogin(context.request, username, "D6 Auto Expand"); const campaign = await postJson(context.request, "/api/campaigns", { name: "D6 Auto Expand", rulesetId: "d6" }); const character = await postJson(context.request, "/api/characters", { name: "Auto Hero", campaignId: campaign.id }); await postJson(context.request, `/api/characters/${character.id}/skills`, { name: "Stealth", diceRollDefinition: "2D+1", wildDice: 1, allowFumble: true }); await page.goto("/"); await expect(page.getByText("Campaign Log")).toBeVisible(); await page.getByRole("button", { name: "Roll Stealth" }).click(); const expandedEntry = page.locator(".log-panel .log-entry.expanded").first(); await expect(expandedEntry).toBeVisible(); await expect(expandedEntry.locator(".log-detail .roll-dice-strip")).toBeVisible(); }); test("Rolemaster UI exposes conditional create and edit fields", async ({ page, context }) => { const username = `rm-ui-${Date.now()}`; await registerAndLogin(context.request, username, "Rolemaster UI"); const campaign = await postJson(context.request, "/api/campaigns", { name: "Rolemaster UI Campaign", rulesetId: "rolemaster" }); const character = await postJson(context.request, "/api/characters", { name: "UI Character", campaignId: campaign.id }); await postJson(context.request, `/api/characters/${character.id}/skill-groups`, { name: "Awareness", diceRollDefinition: "d100!+15", wildDice: 0, allowFumble: false, fumbleRange: 5 }); await postJson(context.request, `/api/characters/${character.id}/skills`, { name: "Perception", diceRollDefinition: "d100!+25", wildDice: 0, allowFumble: false, fumbleRange: 5 }); await page.goto("/"); await expect(page.locator("#workspace-screen-menu-button")).toBeVisible(); await page.locator("#workspace-screen-menu-button").click(); await page.getByRole("menuitem", { name: "Campaign Management" }).click(); await page.getByRole("button", { name: "Add campaign" }).click(); await expect(page.locator("#campaign-ruleset option[value='rolemaster']")).toHaveText("Rolemaster"); await page.getByRole("button", { name: "Cancel" }).click(); await page.locator("#workspace-screen-menu-button").click(); await page.getByRole("menuitem", { name: "Play" }).click(); await page.getByRole("button", { name: "Add group" }).click(); await expect(page.locator("#skill-group-wild-dice")).toHaveCount(0); await expect(page.locator("#skill-group-expression")).toHaveValue("d100"); await page.locator("#skill-group-expression").fill("d100!+15"); await expect(page.locator("#skill-group-fumble-range")).toBeVisible(); await page.locator("#skill-group-fumble-range").fill(""); await page.getByRole("button", { name: "Create Group" }).click(); await expect(page.getByText("Open-ended Rolemaster groups require a fumble range.")).toBeVisible(); await page.getByRole("button", { name: "Cancel" }).click(); await page.getByRole("button", { name: "Add skill" }).first().click(); await expect(page.locator("#skill-create-expression")).toHaveValue("d100!+15"); await page.locator("#skill-create-expression").fill("15d10"); await expect(page.locator("#skill-create-fumble-range")).toHaveCount(0); await page.locator("#skill-create-expression").fill("d100!+25"); await expect(page.locator("#skill-create-fumble-range")).toBeVisible(); await page.getByRole("button", { name: "Cancel" }).click(); await page.locator("button[title='Edit skill']").first().click(); await expect(page.locator("#skill-edit-expression")).toHaveValue("d100!+25"); await expect(page.locator("#skill-edit-fumble-range")).toHaveValue("5"); await page.locator("#skill-edit-expression").fill("d10"); await expect(page.locator("#skill-edit-fumble-range")).toHaveCount(0); await page.getByRole("button", { name: "Cancel" }).click(); });