cents/tests/e2e/测试夹具.ts

126 lines
5.5 KiB
TypeScript

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<string, unknown>;
export function makeEntry(overrides: Partial<Entry> = {}): 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();
}