import { expect, request as playwrightRequest, type APIRequestContext, type Page } from "@playwright/test"; import { TEST_IDS } from "./testIds"; export const E2E_BASE_URL = process.env.E2E_BASE_URL ?? "http://127.0.0.1:3000"; export type E2EUser = { identifier: string; password: string; name: string; }; export const EMPLOYEE: E2EUser = { identifier: "employee@hyinda.com", password: "EmployeePassword1234", name: "Employee", }; export const MANAGER: E2EUser = { identifier: "manager@hyinda.com", password: "ManagerPassword1234", name: "Manager", }; export const FINANCE: E2EUser = { identifier: "finance@hyinda.com", password: "FinancePassword1234", name: "Finance", }; export const FINANCE_MGR: E2EUser = { identifier: "finance_mgr@hyinda.com", password: "FinanceMgrPassword1234", name: "Finance Manager", }; export const ADMIN: E2EUser = { identifier: "admin@hyinda.com", password: "AdminPassword1234", name: "Admin", }; export type MeResponse = { id: string; employeeNo: string; name: string; email: string; roleCodes: string[]; permissions: string[]; profileCompleteness?: { hasEmergencyContact: boolean; hasBankAccount: boolean; hasAvatar: boolean; hasIdNumber: boolean; score: number; }; }; export type AuthenticatedApiClient = { api: APIRequestContext; accessToken: string; me: MeResponse; dispose: () => Promise; }; type Envelope = { data: T; error?: { code?: string; message?: string; details?: unknown; }; }; function buildDeviceFingerprint(user: E2EUser) { return `pw-e2e-${user.identifier.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`; } async function readJson(response: { ok(): boolean; status(): number; text(): Promise }) { const rawText = await response.text(); const parsed = rawText ? (JSON.parse(rawText) as Envelope) : ({ data: null } as Envelope); if (!response.ok()) { const errorCode = parsed.error?.code ?? `HTTP_${response.status()}`; const errorMessage = parsed.error?.message ?? rawText; throw new Error(`${errorCode}: ${errorMessage}`); } return parsed.data; } export async function login(page: Page, user: E2EUser) { await page.goto("/login"); await page.getByTestId(TEST_IDS.login.identifier).fill(user.identifier); await page.getByTestId(TEST_IDS.login.password).fill(user.password); await page.getByTestId(TEST_IDS.login.submit).click(); await expect(page).toHaveURL(/\/portal/); await expect(page.getByTestId(TEST_IDS.topbar.profileTrigger).first()).toBeVisible(); } export async function logout(page: Page, userDisplayName?: string) { try { const profileTrigger = page.getByTestId(TEST_IDS.topbar.profileTrigger).first(); if (await profileTrigger.isVisible().catch(() => false)) { await profileTrigger.click({ force: true }); } else if (userDisplayName) { await page.getByRole("button", { name: userDisplayName, exact: true }).first().click({ force: true }); } else { throw new Error("profile trigger not visible"); } await expect(page.getByRole("menuitem", { name: "退出登录" })).toBeVisible({ timeout: 5_000 }); await page.getByRole("menuitem", { name: "退出登录" }).click(); } catch { await page.context().clearCookies(); await page.evaluate(() => { window.localStorage.clear(); window.sessionStorage.clear(); }); await page.goto("/login"); } await expect(page).toHaveURL(/\/login/); } export async function createAuthenticatedApiClient(user: E2EUser, baseURL = E2E_BASE_URL): Promise { const loginContext = await playwrightRequest.newContext({ baseURL, extraHTTPHeaders: { "x-device-fingerprint": buildDeviceFingerprint(user), }, }); const loginResponse = await loginContext.post("/api/auth/login", { data: { identifier: user.identifier, password: user.password, }, }); const loginData = await readJson<{ accessToken: string }>(loginResponse); const accessToken = loginData.accessToken; const api = await playwrightRequest.newContext({ baseURL, extraHTTPHeaders: { authorization: `Bearer ${accessToken}`, }, }); const meResponse = await api.get("/api/me"); const me = await readJson(meResponse); return { api, accessToken, me, dispose: async () => { await Promise.all([api.dispose(), loginContext.dispose()]); }, }; } export async function createAuthenticatedBrowserStorageState(user: E2EUser, baseURL = E2E_BASE_URL) { const deviceFingerprint = buildDeviceFingerprint(user); const loginContext = await playwrightRequest.newContext({ baseURL, extraHTTPHeaders: { "x-device-fingerprint": deviceFingerprint, }, }); try { const loginResponse = await loginContext.post("/api/auth/login", { data: { identifier: user.identifier, password: user.password, }, }); await readJson<{ accessToken: string }>(loginResponse); const state = await loginContext.storageState(); const origin = new URL(baseURL).origin; const originState = state.origins.find((item) => item.origin === origin); const deviceFingerprintEntry = { name: "hyb_oa_device_fingerprint", value: deviceFingerprint, }; if (originState) { originState.localStorage = originState.localStorage.filter((item) => item.name !== deviceFingerprintEntry.name); originState.localStorage.push(deviceFingerprintEntry); } else { state.origins.push({ origin, localStorage: [deviceFingerprintEntry], }); } return state; } finally { await loginContext.dispose(); } } export async function apiGetData(api: APIRequestContext, path: string) { const response = await api.get(path); return readJson(response); } export async function apiPostData(api: APIRequestContext, path: string, data: unknown) { const response = await api.post(path, { data }); return readJson(response); } export async function apiPatchData(api: APIRequestContext, path: string, data: unknown) { const response = await api.patch(path, { data }); return readJson(response); }