import { expect, type Page } from "@playwright/test"; export const 用户 = { 主用户: { id: "user-primary", name: "测试一", password: "test12345" }, 成员: { id: "user-member", name: "测试二", password: "test12345" }, }; export const 账本 = { 个人: { id: "ledger-personal", name: "个人账本", role: "owner", isPersonal: true }, 家庭: { id: "ledger-family", name: "家庭账本", role: "owner", isPersonal: false }, }; type Entry = Record; export function makeEntry(overrides: Partial = {}): Entry { return { id: "entry-lunch", ownerId: 用户.主用户.id, ledgerIds: [账本.个人.id, 账本.家庭.id], type: "expense", amount: 3200, currency: "CNY", baseCurrency: "CNY", baseAmount: 3200, exchangeRate: "1", exchangeRateDate: "2026-08-01", exchangeRateEffectiveDate: "2026-08-01", exchangeRateSource: "system", conversionStatus: "exact", reimbursementOfEntryId: null, categoryId: "restaurant", note: "午餐", occurredAt: "2026-08-01T12:00:00.000Z", createdBy: 用户.主用户.id, updatedBy: 用户.主用户.id, createdAt: "2026-08-01T12:00:00.000Z", updatedAt: "2026-08-01T12:00:00.000Z", deletedAt: null, version: 1, ...overrides, }; } export type MockOptions = { initialEntries?: Entry[]; currentUser?: typeof 用户.主用户 | typeof 用户.成员 | null; memberRole?: "owner" | "member"; apiAvailable?: boolean; }; export async function mockApi(page: Page, options: MockOptions = {}) { const state = { currentUser: options.currentUser ?? null, entries: [...(options.initialEntries ?? [])], ledgers: [ { ...账本.个人, defaultCurrency: "CNY", theme: "jade", icon: "wallet", archivedAt: null }, { ...账本.家庭, defaultCurrency: "CNY", theme: "coral", icon: "users", archivedAt: null, role: options.memberRole ?? "owner" }, ], apiAvailable: options.apiAvailable ?? true, requestPaths: [] as string[], }; await page.route("**/api/**", async (route) => { const request = route.request(); const url = new URL(request.url()); state.requestPaths.push(`${request.method()} ${url.pathname}`); if (!state.apiAvailable) { await route.abort("failed"); return; } const json = async (body: unknown, status = 200) => route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) }); if (url.pathname === "/api/auth/session") return json({ user: state.currentUser }); if (url.pathname === "/api/auth/login" && request.method() === "POST") { const body = request.postDataJSON() as { name?: string; password?: string }; const account = Object.values(用户).find((item) => item.name === body.name && item.password === body.password); if (!account) return json({ error: "姓名或密码错误" }, 401); state.currentUser = account; return json({ user: account }); } if (url.pathname === "/api/auth/logout") { state.currentUser = null; return json({ ok: true }); } if (url.pathname === "/api/ledgers") { if (request.method() === "POST") { const body = request.postDataJSON() as { name: string; defaultCurrency?: string; theme?: string; icon?: string }; const created = { id: `ledger-${state.entries.length + 10}`, name: body.name, role: "owner", isPersonal: false, defaultCurrency: body.defaultCurrency ?? "CNY", theme: body.theme ?? "jade", icon: body.icon ?? "wallet", archivedAt: null }; state.ledgers.push(created); return json({ ledger: created }, 201); } return json({ ledgers: state.ledgers }); } if (/^\/api\/ledgers\/[^/]+\/members$/.test(url.pathname)) return json({ members: [{ id: 用户.主用户.id, name: 用户.主用户.name, role: options.memberRole ?? "owner", joinedAt: "2026-08-01" }] }); if (url.pathname.startsWith("/api/ledgers/") && request.method() === "PATCH") return json({ ledger: {} }); if (url.pathname === "/api/sync/pull") return json({ entries: state.entries, cursor: "cursor-1", hasMore: false }); if (url.pathname === "/api/sync/push" && request.method() === "POST") { const body = request.postDataJSON() as { operations?: Array<{ id: string; action: string; payload: Entry }> }; for (const operation of body.operations ?? []) { if (operation.action === "delete") continue; const index = state.entries.findIndex((entry) => entry.id === operation.payload.id); if (index >= 0) state.entries[index] = operation.payload; else state.entries.push(operation.payload); } return json({ acceptedOperationIds: (body.operations ?? []).map((operation) => operation.id) }); } if (/^\/api\/exchange-rates\//.test(url.pathname)) { const [, , currency, date] = url.pathname.split("/"); return json({ rate: { currency, requestedDate: date, effectiveDate: date, cnyPerUnit: currency === "USD" ? "7.2" : "1", status: "exact" } }); } if (url.pathname === "/api/me") return json({ user: state.currentUser }); return json({ error: "未模拟的测试 API" }, 404); }); return state; } export async function 登录(page: Page, user = 用户.主用户) { await page.goto("/login"); await page.getByLabel("姓名").fill(user.name); await page.locator('input[autocomplete="current-password"]').fill(user.password); await page.getByRole("button", { name: "登录" }).click(); await expect(page).toHaveURL(/\/$/); await expect(page.getByLabel("账本流水")).toBeVisible(); }