126 lines
5.5 KiB
TypeScript
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();
|
|
}
|