Files
mousehold/apps/web/tests/mvp.spec.ts
2026-06-28 13:48:15 +02:00

103 lines
4.2 KiB
TypeScript

import { expect, test, type APIRequestContext, type APIResponse } from "@playwright/test";
import { readFileSync } from "node:fs";
function envFileValue(key: string): string | undefined {
try {
const lines = readFileSync(".env.development", "utf8").split(/\r?\n/);
const prefix = `${key}=`;
const line = lines.find((item) => item.startsWith(prefix));
return line?.slice(prefix.length).trim().replace(/^["']|["']$/g, "");
} catch {
return undefined;
}
}
const apiBase =
process.env.PLAYWRIGHT_API_URL ??
process.env.VITE_API_URL ??
envFileValue("VITE_API_URL") ??
"http://127.0.0.1:8000";
async function expectOk(response: APIResponse): Promise<void> {
expect(response.ok(), await response.text()).toBeTruthy();
}
async function postJson<T>(
request: APIRequestContext,
path: string,
data: Record<string, unknown>
): Promise<T> {
const response = await request.post(`${apiBase}${path}`, { data });
await expectOk(response);
return (await response.json()) as T;
}
test("local MVP ledger flow", async ({ page, request }) => {
test.setTimeout(60_000);
const suffix = Date.now().toString(36);
const household = await postJson<{ id: string; name: string }>(request, "/households", {
name: `Smoke household ${suffix}`,
default_currency: "EUR"
});
const userA = await postJson<{ id: string; display_name: string }>(request, "/users", {
household_id: household.id,
display_name: `Alice ${suffix}`,
email: `alice-${suffix}@example.test`
});
await postJson<{ id: string }>(request, "/users", {
household_id: household.id,
display_name: `Bob ${suffix}`,
email: `bob-${suffix}@example.test`
});
const account = await postJson<{ id: string; name: string }>(request, "/accounts", {
household_id: household.id,
owner_user_id: userA.id,
name: `Alice checking ${suffix}`,
account_type: "checking",
institution_name: "Smoke Bank",
currency: "EUR",
visibility_mode: "shared_full",
is_active: true
});
try {
await page.goto("/");
const householdSelect = page.getByLabel("Household");
await expect(householdSelect).toContainText(household.name);
await householdSelect.selectOption({ value: household.id });
const sessionSelect = page.getByLabel("Signed in");
await expect(sessionSelect).toBeEnabled();
await expect(sessionSelect).toContainText(userA.display_name);
await sessionSelect.selectOption({ value: userA.id });
await expect(page.getByRole("button", { name: "Logout" })).toBeVisible();
await page.getByRole("button", { name: "Entry" }).click();
const manualEntry = page.locator("section.panel").filter({ has: page.getByRole("heading", { name: "Entry" }) });
await manualEntry.getByLabel("Amount", { exact: true }).fill("42.50");
await manualEntry.getByLabel("Merchant", { exact: true }).fill(`Smoke Shop ${suffix}`);
await manualEntry.getByLabel("Description", { exact: true }).fill("Smoke groceries");
await manualEntry.locator('select[name="payer_user_id"]').selectOption({ label: userA.display_name });
await manualEntry.locator('select[name="source_account_id"]').selectOption({ label: account.name });
await manualEntry.getByRole("button", { name: "Observation" }).click();
await page.getByRole("button", { name: "Inbox" }).click();
const reviewCard = page.locator(".review-card").filter({ hasText: `Smoke Shop ${suffix}` }).first();
await expect(reviewCard).toBeVisible();
await reviewCard.getByRole("button", { name: "Confirm" }).click();
await page.getByRole("button", { name: "Ledger" }).click();
const ledgerRow = page.locator("tbody tr").filter({ hasText: `Smoke Shop ${suffix}` }).first();
await expect(ledgerRow).toBeVisible();
await expect(ledgerRow.getByText("confirmed")).toBeVisible();
await page.getByRole("button", { name: "Setup" }).click();
await expect(page.getByRole("button", { name: "Export JSON" })).toBeEnabled();
await expect(page.getByRole("button", { name: "SQLite backup" })).toBeEnabled();
} finally {
const cleanup = await request.delete(`${apiBase}/households/${household.id}`);
if (!cleanup.ok() && cleanup.status() !== 404) {
console.warn(await cleanup.text());
}
}
});