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 { expect(response.ok(), await response.text()).toBeTruthy(); } async function postJson( request: APIRequestContext, path: string, data: Record ): Promise { 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()); } } });