Files
RpgRoller/tests/e2e/smoke.spec.js

344 lines
13 KiB
JavaScript

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("home document renders static auth markup without bootstrapping blazor", async ({ request }) => {
const response = await request.get("/");
expect(response.ok()).toBeTruthy();
const html = await response.text();
expect(html).not.toContain("Connecting...");
expect(html).toContain("Register or log in to join a campaign session.");
expect(html).not.toContain("_framework/blazor.web.js");
expect(html).not.toContain("<!--Blazor:");
expect(html).toContain("data-auth-page");
});
test("successful login transitions to play workspace", async ({ page, context }) => {
const username = `login-${Date.now()}`;
const password = "Password123";
await postJson(context.request, "/api/auth/register", {
username,
password,
displayName: "Login Flow"
});
await page.goto("/");
await page.locator("#login-username").fill(username);
await page.locator("#login-password").fill(password);
await page.getByRole("button", { name: "Login" }).click();
await expect(page.getByText("Campaign Log")).toBeVisible();
await expect(page.locator("#login-username")).toHaveCount(0);
});
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-entry").first()).toBeVisible();
await expect(page.locator(".log-panel .log-event-badge")).toContainText(["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("Rolemaster automatic retry badge shows before detail expands", async ({ page, context }) => {
const username = `rm-retry-${Date.now()}`;
await registerAndLogin(context.request, username, "Rolemaster Retry Smoke");
const campaign = await postJson(context.request, "/api/campaigns", {
name: "Rolemaster Retry Smoke",
rulesetId: "rolemaster"
});
const character = await postJson(context.request, "/api/characters", {
name: "Retry Hero",
campaignId: campaign.id
});
const skill = await postJson(context.request, `/api/characters/${character.id}/skills`, {
name: "Retry Sight",
diceRollDefinition: "d100!+10",
wildDice: 0,
allowFumble: false,
fumbleRange: 5,
rolemasterAutoRetry: true
});
let retriedRoll = null;
for (let attempt = 0; attempt < 10; attempt += 1) {
const roll = await postJson(context.request, `/api/skills/${skill.id}/roll`, { visibility: "public" });
if (roll.breakdown.includes("retry(+")) {
retriedRoll = roll;
break;
}
}
expect(retriedRoll, "expected a retry-enabled Rolemaster roll within 10 attempts").not.toBeNull();
await page.goto("/");
await expect(page.getByText("Campaign Log")).toBeVisible();
const retryEntry = page.locator(".log-panel .log-entry").filter({ hasText: "retry +" }).last();
await expect(retryEntry).toBeVisible();
await expect(retryEntry.locator(".log-event-badge")).toContainText([/Retry \+(5|10)/]);
await expect(retryEntry.locator(".log-summary-text")).toContainText(/retry \+(5|10)/);
await expect(retryEntry.locator(".log-detail")).toHaveCount(0);
await retryEntry.locator(".log-entry-toggle").click();
const detailDice = retryEntry.locator(".log-detail .die-chip");
await expect(detailDice).toHaveCount(2);
await expect(detailDice.nth(0)).toHaveAttribute("title", /attempt 1/i);
await expect(detailDice.nth(1)).toHaveAttribute("title", /retry attempt 2/i);
});
test("Rolemaster skill roll modal autofocuses, validates, and closes on escape or backdrop", async ({ page, context }) => {
const username = `rm-modal-${Date.now()}`;
await registerAndLogin(context.request, username, "Rolemaster Modal Smoke");
const campaign = await postJson(context.request, "/api/campaigns", {
name: "Rolemaster Modal Smoke",
rulesetId: "rolemaster"
});
const character = await postJson(context.request, "/api/characters", {
name: "Observer",
campaignId: campaign.id
});
await postJson(context.request, `/api/characters/${character.id}/skills`, {
name: "Observation",
diceRollDefinition: "d100!+50",
wildDice: 0,
allowFumble: false,
fumbleRange: 5,
rolemasterAutoRetry: true
});
await page.goto("/");
await expect(page.getByText("Campaign Log")).toBeVisible();
const rollButton = page.getByRole("button", { name: "Roll Observation" });
const modal = page.getByRole("dialog", { name: "Rolemaster situational modifier" });
const modifierInput = page.locator("#rolemaster-situational-modifier");
await rollButton.click();
await expect(modal).toBeVisible();
await expect(modifierInput).toBeFocused();
await page.keyboard.press("Escape");
await expect(modal).toHaveCount(0);
await rollButton.click();
await expect(modal).toBeVisible();
await page.locator(".modal-overlay").click({ position: { x: 8, y: 8 } });
await expect(modal).toHaveCount(0);
await rollButton.click();
await expect(modal).toBeVisible();
await modifierInput.fill("1001");
await modal.getByRole("button", { name: "Roll" }).click();
await expect(page.getByText("Enter a whole number between -1000 and 1000.")).toBeVisible();
await expect(modal).toBeVisible();
await modifierInput.fill("");
await page.keyboard.press("Enter");
await expect(modal).toHaveCount(0);
await expect(page.locator(".log-panel .log-entry.expanded").first()).toContainText("Observation");
});
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("custom roll composer keeps parse errors inline and records successful rolls", async ({ page, context }) => {
const username = `custom-roll-${Date.now()}`;
await registerAndLogin(context.request, username, "Custom Roller");
const campaign = await postJson(context.request, "/api/campaigns", {
name: "Custom Roll Campaign",
rulesetId: "dnd5e"
});
await postJson(context.request, "/api/characters", {
name: "Improviser",
campaignId: campaign.id
});
await page.goto("/");
await expect(page.getByText("Campaign Log")).toBeVisible();
const composer = page.locator(".custom-roll-composer");
const input = page.locator("#custom-roll-expression");
await input.fill("bad");
await composer.getByRole("button", { name: "Roll" }).click();
await expect(input).toHaveClass(/error/);
await expect(input).toHaveAttribute("title", /Expected dnd5e format like 2d12\+2\./);
await expect(page.locator(".toast.error")).toHaveCount(0);
await input.fill("1d20+5");
await composer.getByRole("button", { name: "Roll" }).click();
await expect(input).not.toHaveClass(/error/);
await expect(page.locator(".log-panel .log-entry").first()).toContainText("Custom roll");
});
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,
rolemasterAutoRetry: true
});
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 expect(page.getByLabel("Automatic retry")).toHaveCount(0);
await page.locator("#skill-create-expression").fill("d100!+25");
await expect(page.locator("#skill-create-fumble-range")).toBeVisible();
await expect(page.getByLabel("Automatic retry")).toBeVisible();
await page.getByLabel("Automatic retry").check();
await page.locator("#skill-create-expression").fill("d10");
await expect(page.getByLabel("Automatic retry")).toHaveCount(0);
await page.locator("#skill-create-expression").fill("d100!+25");
await expect(page.getByLabel("Automatic retry")).toBeVisible();
await expect(page.getByLabel("Automatic retry")).not.toBeChecked();
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 expect(page.getByLabel("Automatic retry")).toBeChecked();
await page.locator("#skill-edit-expression").fill("d10");
await expect(page.locator("#skill-edit-fumble-range")).toHaveCount(0);
await expect(page.getByLabel("Automatic retry")).toHaveCount(0);
await page.locator("#skill-edit-expression").fill("d100!+25");
await expect(page.getByLabel("Automatic retry")).toBeVisible();
await expect(page.getByLabel("Automatic retry")).not.toBeChecked();
await page.getByRole("button", { name: "Cancel" }).click();
});