文件
hyb-oa/client/tests/e2e/helpers/auth.ts
T
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
2026-09-19 13:11:04 +08:00

225 行
6.6 KiB
TypeScript

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;
employeeNo?: string;
};
export const EMPLOYEE: E2EUser = {
identifier: "[email protected]",
password: "EmployeePassword1234",
name: "陈工程",
employeeNo: "HYD000003",
};
export const MANAGER: E2EUser = {
identifier: "[email protected]",
password: "ManagerPassword1234",
name: "李研发",
employeeNo: "HYD000002",
};
export const FINANCE: E2EUser = {
identifier: "[email protected]",
password: "FinancePassword1234",
name: "赵财务",
employeeNo: "HYD000004",
};
export const FINANCE_MGR: E2EUser = {
identifier: "[email protected]",
password: "FinanceMgrPassword1234",
name: "钱主管",
employeeNo: "HYD000005",
};
export const ADMIN: E2EUser = {
identifier: "[email protected]",
password: "AdminPassword1234",
name: "王顺利",
employeeNo: "HYD000006",
};
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<void>;
};
type Envelope<T> = {
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<T>(response: { ok(): boolean; status(): number; text(): Promise<string> }) {
const rawText = await response.text();
const parsed = rawText ? (JSON.parse(rawText) as Envelope<T>) : ({ data: null } as Envelope<T>);
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, { reuseLoginPage = false } = {}) {
// Opt-in for a sequential UI flow whose preceding logout already reached
// the canonical page without a redirect. Form and portal checks stay unchanged.
const current = new URL(page.url());
if (!reuseLoginPage || current.pathname !== "/login" || current.search || current.hash) 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<AuthenticatedApiClient> {
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>(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<T>(api: APIRequestContext, path: string) {
const response = await api.get(path);
return readJson<T>(response);
}
export async function apiPostData<T>(api: APIRequestContext, path: string, data: unknown) {
const response = await api.post(path, { data });
return readJson<T>(response);
}
export async function apiPatchData<T>(api: APIRequestContext, path: string, data: unknown) {
const response = await api.patch(path, { data });
return readJson<T>(response);
}