222 行
6.2 KiB
TypeScript
222 行
6.2 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) {
|
|
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);
|
|
}
|