R31-1 R31-4: 收口详情页工作台化与 E2E 契约
这个提交包含在:
@@ -34,6 +34,12 @@ export const FINANCE_MGR: E2EUser = {
|
||||
name: "Finance Manager",
|
||||
};
|
||||
|
||||
export const ADMIN: E2EUser = {
|
||||
identifier: "[email protected]",
|
||||
password: "AdminPassword1234",
|
||||
name: "Admin",
|
||||
};
|
||||
|
||||
export type MeResponse = {
|
||||
id: string;
|
||||
employeeNo: string;
|
||||
@@ -80,6 +86,7 @@ export async function login(page: Page, user: E2EUser) {
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { test as base } from "@playwright/test";
|
||||
|
||||
import { AppShell } from "../pageObjects/AppShell";
|
||||
import { ReimbursementDetailPageObject } from "../pageObjects/ReimbursementDetailPage";
|
||||
import { WorkflowTaskDetailPageObject } from "../pageObjects/WorkflowTaskDetailPage";
|
||||
|
||||
export const test = base.extend<{
|
||||
shell: AppShell;
|
||||
reimbursementDetail: ReimbursementDetailPageObject;
|
||||
workflowDetail: WorkflowTaskDetailPageObject;
|
||||
}>({
|
||||
shell: async ({ page }, use) => {
|
||||
await use(new AppShell(page));
|
||||
},
|
||||
reimbursementDetail: async ({ page }, use) => {
|
||||
await use(new ReimbursementDetailPageObject(page));
|
||||
},
|
||||
workflowDetail: async ({ page }, use) => {
|
||||
await use(new WorkflowTaskDetailPageObject(page));
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
@@ -0,0 +1,45 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
import { openBackupModulesIfNeeded, openNavLink, openProfileSettings } from "../navigation";
|
||||
import { TEST_IDS } from "../testIds";
|
||||
|
||||
export class AppShell {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
get sidebar() {
|
||||
return this.page.locator("nav").first();
|
||||
}
|
||||
|
||||
get topbarProfileTrigger() {
|
||||
return this.page.getByTestId(TEST_IDS.topbar.profileTrigger).first();
|
||||
}
|
||||
|
||||
async navigate(name: string | RegExp) {
|
||||
return openNavLink(this.page, name);
|
||||
}
|
||||
|
||||
async openBackupModules() {
|
||||
return openBackupModulesIfNeeded(this.page);
|
||||
}
|
||||
|
||||
async openProfile(userDisplayName?: string) {
|
||||
return openProfileSettings(this.page, userDisplayName);
|
||||
}
|
||||
|
||||
async assertMainNavVisible() {
|
||||
await expect(this.page.getByRole("link", { name: "工作台" }).or(this.page.getByRole("link", { name: "Portal" })).first()).toBeVisible();
|
||||
await expect(this.page.getByRole("link", { name: /项目/ }).first()).toBeVisible();
|
||||
}
|
||||
|
||||
async assertBackupModulesCollapsed() {
|
||||
const summary = this.page.locator("summary").filter({ hasText: "备用模块" }).first();
|
||||
if (await summary.count()) {
|
||||
const details = summary.locator("xpath=ancestor::details[1]");
|
||||
await expect(details).not.toHaveAttribute("open", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
import { TEST_IDS } from "../testIds";
|
||||
|
||||
export class ReimbursementDetailPageObject {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
get overview() {
|
||||
return this.page.getByTestId(TEST_IDS.reimbursement.overview);
|
||||
}
|
||||
|
||||
get summary() {
|
||||
return this.page.getByTestId(TEST_IDS.reimbursement.summary);
|
||||
}
|
||||
|
||||
get itemsTable() {
|
||||
return this.page.getByTestId(TEST_IDS.reimbursement.items);
|
||||
}
|
||||
|
||||
get addItemBtn() {
|
||||
return this.page.getByTestId(TEST_IDS.reimbursement.itemAdd);
|
||||
}
|
||||
|
||||
get budgetHint() {
|
||||
return this.page.getByTestId(TEST_IDS.reimbursement.budgetHint);
|
||||
}
|
||||
|
||||
get erpExport() {
|
||||
return this.page.getByTestId(TEST_IDS.reimbursement.erpExport);
|
||||
}
|
||||
|
||||
get submitBtn() {
|
||||
return this.page.getByTestId(TEST_IDS.reimbursement.submit);
|
||||
}
|
||||
|
||||
get withdrawBtn() {
|
||||
return this.page.getByTestId(TEST_IDS.reimbursement.withdraw);
|
||||
}
|
||||
|
||||
get payBtn() {
|
||||
return this.page.getByTestId(TEST_IDS.reimbursement.pay);
|
||||
}
|
||||
|
||||
async assertLoaded() {
|
||||
await expect(this.overview).toBeVisible({ timeout: 15_000 });
|
||||
await expect(this.summary).toBeVisible({ timeout: 15_000 });
|
||||
}
|
||||
|
||||
async assertStatus(statusText: string) {
|
||||
await expect(this.summary).toContainText(statusText);
|
||||
}
|
||||
|
||||
// 当前 seed 保证 precheck 不触发 warn/manual_review;若 seed 变更,此处需相应调整。
|
||||
async submitReimbursement(acceptConfirm = true) {
|
||||
const dialogPromise = acceptConfirm
|
||||
? this.page.waitForEvent("dialog", { timeout: 2_000 }).then((dialog) => dialog.accept()).catch(() => undefined)
|
||||
: Promise.resolve();
|
||||
|
||||
await this.submitBtn.click();
|
||||
await dialogPromise;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
import { TEST_IDS } from "../testIds";
|
||||
|
||||
export class WorkflowTaskDetailPageObject {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
get overview() {
|
||||
return this.page.getByTestId(TEST_IDS.workflow.overview);
|
||||
}
|
||||
|
||||
get summary() {
|
||||
return this.page.getByTestId(TEST_IDS.workflow.summary);
|
||||
}
|
||||
|
||||
get approveBtn() {
|
||||
return this.page.getByTestId(TEST_IDS.workflow.approve);
|
||||
}
|
||||
|
||||
get rejectCommentInput() {
|
||||
return this.page.getByTestId(TEST_IDS.workflow.rejectComment);
|
||||
}
|
||||
|
||||
get rejectBtn() {
|
||||
return this.page.getByTestId(TEST_IDS.workflow.reject);
|
||||
}
|
||||
|
||||
get returnCommentInput() {
|
||||
return this.page.getByTestId(TEST_IDS.workflow.returnComment);
|
||||
}
|
||||
|
||||
get returnBtn() {
|
||||
return this.page.getByTestId(TEST_IDS.workflow.returnBtn);
|
||||
}
|
||||
|
||||
get actionError() {
|
||||
return this.page.getByTestId(TEST_IDS.workflow.actionError);
|
||||
}
|
||||
|
||||
get transferApproverSelect() {
|
||||
return this.page.getByTestId(TEST_IDS.workflow.transferApprover);
|
||||
}
|
||||
|
||||
async assertLoaded() {
|
||||
await expect(this.overview).toBeVisible({ timeout: 15_000 });
|
||||
await expect(this.summary).toBeVisible({ timeout: 15_000 });
|
||||
}
|
||||
|
||||
async approve() {
|
||||
await this.approveBtn.click();
|
||||
}
|
||||
|
||||
async reject(comment: string) {
|
||||
await this.rejectCommentInput.fill(comment);
|
||||
await this.rejectBtn.click();
|
||||
}
|
||||
|
||||
async returnTask(comment: string) {
|
||||
await this.returnCommentInput.fill(comment);
|
||||
await this.returnBtn.click();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1 @@
|
||||
export const TEST_IDS = {
|
||||
login: {
|
||||
identifier: "login-identifier",
|
||||
password: "login-password",
|
||||
submit: "login-submit",
|
||||
},
|
||||
topbar: {
|
||||
profileTrigger: "topbar-profile-trigger",
|
||||
},
|
||||
workflow: {
|
||||
tasksTable: "workflow-tasks-table",
|
||||
taskRow: "workflow-task-row",
|
||||
detailLink: "workflow-task-detail-link",
|
||||
summary: "workflow-task-summary",
|
||||
approve: "workflow-task-approve",
|
||||
},
|
||||
audit: {
|
||||
export: "audit-export",
|
||||
},
|
||||
} as const;
|
||||
export { TEST_IDS } from "../../../src/lib/testIds";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "./helpers/fixtures";
|
||||
import { EMPLOYEE, MANAGER, apiPostData, createAuthenticatedApiClient, login, logout } from "./helpers/auth";
|
||||
import { openNavLink } from "./helpers/navigation";
|
||||
import { TEST_IDS } from "./helpers/testIds";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
|
||||
function formatDate(offsetDays: number) {
|
||||
const value = new Date();
|
||||
@@ -69,7 +69,7 @@ async function withdrawLeave(api: Awaited<ReturnType<typeof createAuthenticatedA
|
||||
return apiPostData<{ leaveRequestId: string; leaveStatus: string }>(api, `/api/leave-requests/${leaveRequestId}/withdraw`, {});
|
||||
}
|
||||
|
||||
test("E2E 请假主链路:草稿 → 提交 → 撤回 + 提交 → 审批", async ({ page }) => {
|
||||
test("E2E 请假主链路:草稿 → 提交 → 撤回 + 提交 → 审批", async ({ page, workflowDetail }) => {
|
||||
const employeeClient = await createAuthenticatedApiClient(EMPLOYEE);
|
||||
const baseOffset = 30 + Number(String(Date.now()).slice(-2));
|
||||
const firstDate = formatDate(baseOffset);
|
||||
@@ -77,42 +77,43 @@ test("E2E 请假主链路:草稿 → 提交 → 撤回 + 提交 → 审批", a
|
||||
|
||||
try {
|
||||
await login(page, EMPLOYEE);
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
|
||||
const firstLeaveId = await createLeaveDraft(page, firstDate, firstDate, `Playwright 撤回请假 ${Date.now()}`);
|
||||
await submitLeave(employeeClient.api, firstLeaveId);
|
||||
await page.goto("/leave-requests");
|
||||
const firstRow = page.getByRole("row").filter({ hasText: firstDate }).first();
|
||||
await expect(firstRow).toContainText("审批中", { timeout: 30_000 });
|
||||
const firstLeaveId = await createLeaveDraft(page, firstDate, firstDate, `Playwright 撤回请假 ${Date.now()}`);
|
||||
await submitLeave(employeeClient.api, firstLeaveId);
|
||||
await page.goto("/leave-requests");
|
||||
const firstRow = page.getByRole("row").filter({ hasText: firstDate }).first();
|
||||
await expect(firstRow).toContainText("审批中", { timeout: 30_000 });
|
||||
|
||||
await withdrawLeave(employeeClient.api, firstLeaveId);
|
||||
await page.goto("/leave-requests");
|
||||
await expect(firstRow).toContainText("草稿", { timeout: 30_000 });
|
||||
await withdrawLeave(employeeClient.api, firstLeaveId);
|
||||
await page.goto("/leave-requests");
|
||||
await expect(firstRow).toContainText("草稿", { timeout: 30_000 });
|
||||
|
||||
const secondLeaveId = await createLeaveDraft(page, secondDate, secondDate, `Playwright 审批请假 ${Date.now()}`);
|
||||
const secondSubmit = await submitLeave(employeeClient.api, secondLeaveId);
|
||||
await page.goto("/leave-requests");
|
||||
const secondRow = page.getByRole("row").filter({ hasText: secondDate }).first();
|
||||
await expect(secondRow).toContainText("审批中", { timeout: 30_000 });
|
||||
const secondLeaveId = await createLeaveDraft(page, secondDate, secondDate, `Playwright 审批请假 ${Date.now()}`);
|
||||
const secondSubmit = await submitLeave(employeeClient.api, secondLeaveId);
|
||||
await page.goto("/leave-requests");
|
||||
const secondRow = page.getByRole("row").filter({ hasText: secondDate }).first();
|
||||
await expect(secondRow).toContainText("审批中", { timeout: 30_000 });
|
||||
|
||||
await logout(page, EMPLOYEE.name);
|
||||
await logout(page, EMPLOYEE.name);
|
||||
|
||||
await login(page, MANAGER);
|
||||
await (await openNavLink(page, "待我处理")).click();
|
||||
await expect(page.getByTestId(TEST_IDS.workflow.tasksTable)).toBeVisible();
|
||||
const taskRow = page.getByTestId(TEST_IDS.workflow.taskRow).filter({ hasText: secondSubmit.currentTaskId }).first();
|
||||
await expect(taskRow).toBeVisible({ timeout: 30_000 });
|
||||
await taskRow.getByTestId(TEST_IDS.workflow.detailLink).click();
|
||||
await page.getByTestId(TEST_IDS.workflow.approve).click();
|
||||
await expect(page.getByTestId(TEST_IDS.workflow.summary)).toContainText(/状态:(completed|已完成)/);
|
||||
await login(page, MANAGER);
|
||||
await (await openNavLink(page, "待我处理")).click();
|
||||
await expect(page.getByTestId(TEST_IDS.workflow.tasksTable)).toBeVisible();
|
||||
const taskRow = page.getByTestId(TEST_IDS.workflow.taskRow).filter({ hasText: secondSubmit.currentTaskId }).first();
|
||||
await expect(taskRow).toBeVisible({ timeout: 30_000 });
|
||||
await taskRow.getByTestId(TEST_IDS.workflow.detailLink).click();
|
||||
await workflowDetail.assertLoaded();
|
||||
await workflowDetail.approve();
|
||||
await expect(workflowDetail.summary).toContainText(/状态:(completed|已完成)/);
|
||||
|
||||
await logout(page, MANAGER.name);
|
||||
await logout(page, MANAGER.name);
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
await page.goto("/leave-requests");
|
||||
await expect(page.getByRole("row").filter({ hasText: secondDate }).first()).toContainText("已通过", { timeout: 30_000 });
|
||||
await expect(page.getByRole("row").filter({ hasText: firstDate }).first()).toContainText("草稿", { timeout: 30_000 });
|
||||
await login(page, EMPLOYEE);
|
||||
await page.goto("/leave-requests");
|
||||
await expect(page.getByRole("row").filter({ hasText: secondDate }).first()).toContainText("已通过", { timeout: 30_000 });
|
||||
await expect(page.getByRole("row").filter({ hasText: firstDate }).first()).toContainText("草稿", { timeout: 30_000 });
|
||||
} finally {
|
||||
await employeeClient.dispose();
|
||||
}
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "./helpers/fixtures";
|
||||
import { EMPLOYEE, FINANCE, MANAGER, login, logout } from "./helpers/auth";
|
||||
import { openNavLink } from "./helpers/navigation";
|
||||
|
||||
const EMPLOYEE = { identifier: "[email protected]", password: "EmployeePassword1234", name: "Employee" };
|
||||
const MANAGER = { identifier: "[email protected]", password: "ManagerPassword1234", name: "Manager" };
|
||||
const FINANCE = { identifier: "[email protected]", password: "FinancePassword1234", name: "Finance" };
|
||||
|
||||
async function login(page: Page, user: { identifier: string; password: string }) {
|
||||
await page.goto("/login");
|
||||
await page.getByTestId("login-identifier").fill(user.identifier);
|
||||
await page.getByTestId("login-password").fill(user.password);
|
||||
await page.getByTestId("login-submit").click();
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
}
|
||||
|
||||
async function logout(page: Page, userDisplayName: string) {
|
||||
await page.getByRole("button", { name: userDisplayName }).click();
|
||||
await page.getByRole("menuitem", { name: "退出登录" }).click();
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
}
|
||||
import { TEST_IDS } from "./helpers/testIds";
|
||||
|
||||
function extractId(text: string, label: string) {
|
||||
const match = text.match(new RegExp(`${label}:([0-9a-f-]{36})`));
|
||||
@@ -28,11 +11,13 @@ function extractId(text: string, label: string) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登出", async ({ page }) => {
|
||||
test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登出", async ({ page, reimbursementDetail, workflowDetail }) => {
|
||||
// 1) employee: create reimbursement
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await page.locator("nav").getByRole("link", { name: "我的报销", exact: true }).click();
|
||||
await (await openNavLink(page, "我的报销")).click();
|
||||
await expect(page).toHaveURL(/\/reimbursements/);
|
||||
await expect(page.getByTestId("reimbursements-create")).toBeVisible();
|
||||
const createResponsePromise = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/reimbursements" && response.ok();
|
||||
@@ -44,9 +29,7 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
|
||||
throw new Error(`创建报销单后未返回 id: ${JSON.stringify(createPayload)}`);
|
||||
}
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await expect(page.getByTestId("reimbursement-summary")).toBeVisible();
|
||||
|
||||
const reimbursementSummaryText = await page.getByTestId("reimbursement-summary").innerText();
|
||||
await reimbursementDetail.assertLoaded();
|
||||
|
||||
// 2) employee: upload invoice and wait OCR
|
||||
await (await openNavLink(page, "票据中心")).click();
|
||||
@@ -82,7 +65,8 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
|
||||
|
||||
// 4) submit reimbursement
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await page.getByTestId("reimbursement-submit").click();
|
||||
await reimbursementDetail.assertLoaded();
|
||||
await reimbursementDetail.submitReimbursement();
|
||||
await expect(page.getByText(/状态:(reviewing|审批中)/)).toBeVisible();
|
||||
|
||||
// 5) manager approve
|
||||
@@ -90,28 +74,31 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
|
||||
await login(page, MANAGER);
|
||||
|
||||
await (await openNavLink(page, "待我处理")).click();
|
||||
await expect(page.getByTestId("workflow-tasks-table")).toBeVisible();
|
||||
const managerTaskRow = page.getByTestId("workflow-task-row").filter({ hasText: reimbursementId }).first();
|
||||
await expect(page.getByTestId(TEST_IDS.workflow.tasksTable)).toBeVisible();
|
||||
const managerTaskRow = page.getByTestId(TEST_IDS.workflow.taskRow).filter({ hasText: reimbursementId }).first();
|
||||
await expect(managerTaskRow).toBeVisible();
|
||||
await managerTaskRow.getByTestId("workflow-task-detail-link").click();
|
||||
await page.getByTestId("workflow-task-approve").click();
|
||||
await expect(page.getByTestId("workflow-task-summary")).toContainText(/状态:(completed|已完成)/);
|
||||
await managerTaskRow.getByTestId(TEST_IDS.workflow.detailLink).click();
|
||||
await workflowDetail.assertLoaded();
|
||||
await workflowDetail.approve();
|
||||
await expect(workflowDetail.summary).toContainText(/状态:(completed|已完成)/);
|
||||
|
||||
await logout(page, MANAGER.name);
|
||||
|
||||
// 6) finance approve + pay
|
||||
await login(page, FINANCE);
|
||||
await (await openNavLink(page, "待我处理")).click();
|
||||
const financeTaskRow = page.getByTestId("workflow-task-row").filter({ hasText: reimbursementId }).first();
|
||||
const financeTaskRow = page.getByTestId(TEST_IDS.workflow.taskRow).filter({ hasText: reimbursementId }).first();
|
||||
await expect(financeTaskRow).toBeVisible();
|
||||
await financeTaskRow.getByTestId("workflow-task-detail-link").click();
|
||||
await page.getByTestId("workflow-task-approve").click();
|
||||
await expect(page.getByTestId("workflow-task-summary")).toContainText(/状态:(completed|已完成)/);
|
||||
await financeTaskRow.getByTestId(TEST_IDS.workflow.detailLink).click();
|
||||
await workflowDetail.assertLoaded();
|
||||
await workflowDetail.approve();
|
||||
await expect(workflowDetail.summary).toContainText(/状态:(completed|已完成)/);
|
||||
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await reimbursementDetail.assertLoaded();
|
||||
await expect(page.getByText(/状态:(approved|已批准)/)).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId("reimbursement-pay-voucher-no").fill(`E2E-${Date.now()}`);
|
||||
await page.getByTestId("reimbursement-pay").click();
|
||||
await reimbursementDetail.payBtn.click();
|
||||
await expect(page.getByText(/状态:(paid|已支付)/)).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await logout(page, FINANCE.name);
|
||||
|
||||
@@ -14,15 +14,22 @@ async function waitForWorkflowTaskCache(page: import("@playwright/test").Page, t
|
||||
request.onerror = () => reject(request.error ?? new Error("indexeddb_open_failed"));
|
||||
});
|
||||
|
||||
const store = db.transaction("workflow_task_cache", "readonly").objectStore("workflow_task_cache");
|
||||
const detail = await new Promise<unknown>((resolve, reject) => {
|
||||
const query = store.get(currentTaskId);
|
||||
query.onsuccess = () => resolve(query.result ?? null);
|
||||
query.onerror = () => reject(query.error ?? new Error("indexeddb_read_failed"));
|
||||
});
|
||||
try {
|
||||
if (!db.objectStoreNames.contains("workflow_task_cache")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
db.close();
|
||||
return Boolean(detail);
|
||||
const store = db.transaction("workflow_task_cache", "readonly").objectStore("workflow_task_cache");
|
||||
const detail = await new Promise<unknown>((resolve, reject) => {
|
||||
const query = store.get(currentTaskId);
|
||||
query.onsuccess = () => resolve(query.result ?? null);
|
||||
query.onerror = () => reject(query.error ?? new Error("indexeddb_read_failed"));
|
||||
});
|
||||
|
||||
return Boolean(detail);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}, taskId);
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { login } from "./helpers/auth";
|
||||
import { assertBackupModulesCollapsed } from "./helpers/smokeAssertions";
|
||||
import { test, expect } from "./helpers/fixtures";
|
||||
import { ADMIN, login } from "./helpers/auth";
|
||||
import { TEST_IDS } from "./helpers/testIds";
|
||||
|
||||
const ADMIN = {
|
||||
identifier: "[email protected]",
|
||||
password: "AdminPassword1234",
|
||||
name: "Admin",
|
||||
};
|
||||
|
||||
test("E2E 管理域 smoke:工作区设置、系统设置、功能开关、流程定义、审计日志", async ({ page }) => {
|
||||
test("E2E 管理域 smoke:工作区设置、系统设置、功能开关、流程定义、审计日志", async ({ page, shell }) => {
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/portal");
|
||||
await assertBackupModulesCollapsed(page);
|
||||
await expect(shell.topbarProfileTrigger).toBeVisible();
|
||||
await shell.assertBackupModulesCollapsed();
|
||||
|
||||
await page.goto("/system/workspace");
|
||||
await (await shell.navigate("工作区设置")).click();
|
||||
await expect(page).toHaveURL(/\/system\/workspace/);
|
||||
await expect(page.getByRole("heading", { name: "工作区设置" })).toBeVisible();
|
||||
await expect(page.getByText("产品模式")).toBeVisible();
|
||||
|
||||
await page.goto("/system/settings");
|
||||
await (await shell.navigate("系统设置")).click();
|
||||
await expect(page).toHaveURL(/\/system\/settings/);
|
||||
await expect(page.getByRole("heading", { name: "系统设置" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "工作区设置", exact: true }).first()).toBeVisible();
|
||||
|
||||
await page.goto("/system/feature-flags");
|
||||
await (await shell.navigate("功能开关")).click();
|
||||
await expect(page).toHaveURL(/\/system\/feature-flags/);
|
||||
await expect(page.getByRole("heading", { name: "功能开关" })).toBeVisible();
|
||||
await expect(page.getByTestId("feature-flags-table")).toBeVisible();
|
||||
|
||||
await page.goto("/system/workflow-definitions");
|
||||
await expect(page).toHaveURL(/\/system\/workflow-definitions/);
|
||||
await expect(page.getByRole("heading", { name: "流程定义" })).toBeVisible();
|
||||
await expect(page.getByText("版本列表")).toBeVisible();
|
||||
|
||||
await page.goto("/audit-logs");
|
||||
await (await shell.navigate("审计日志")).click();
|
||||
await expect(page).toHaveURL(/\/audit-logs/);
|
||||
await expect(page.getByRole("heading", { name: "审计工作台" })).toBeVisible();
|
||||
await expect(page.getByTestId(TEST_IDS.audit.export)).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { test, expect } from "./helpers/fixtures";
|
||||
|
||||
import { EMPLOYEE, login, logout } from "./helpers/auth";
|
||||
import { openNavLink } from "./helpers/navigation";
|
||||
import { TEST_IDS } from "./helpers/testIds";
|
||||
|
||||
function formatWeekStart(offsetDays: number) {
|
||||
const value = new Date();
|
||||
value.setUTCDate(value.getUTCDate() + offsetDays);
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
test("E2E 周报待办联动:保存 → 生成待办 → 提交 → 完成待办", async ({ page }) => {
|
||||
const uniqueToken = `Playwright 周报联动 ${Date.now()}`;
|
||||
const weekStart = formatWeekStart(35 + Number(String(Date.now()).slice(-1)));
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
await (await openNavLink(page, "周报")).click();
|
||||
await expect(page.getByRole("heading", { name: "周报", exact: true })).toBeVisible();
|
||||
|
||||
const weeklyLoadResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "GET" && url.pathname === "/api/weekly-reports/me" && url.searchParams.get("weekStart") === weekStart && response.ok();
|
||||
});
|
||||
await page.getByLabel("周报归档周").fill(weekStart);
|
||||
await weeklyLoadResponse;
|
||||
await page.getByLabel("本周完成").fill(`${uniqueToken} 已完成`);
|
||||
await page.getByLabel("当前阻塞").fill(`${uniqueToken} 阻塞`);
|
||||
await page.getByLabel("下周计划").fill(`${uniqueToken} 下周计划`);
|
||||
|
||||
const saveResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "PUT" && url.pathname === "/api/weekly-reports/me" && response.ok();
|
||||
});
|
||||
await page.getByTestId(TEST_IDS.weeklyReport.save).click();
|
||||
await saveResponse;
|
||||
await expect(page.getByText("周报已保存")).toBeVisible();
|
||||
|
||||
const createTodoResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/todos" && response.ok();
|
||||
});
|
||||
await page.getByTestId(TEST_IDS.weeklyReport.createTodo).click();
|
||||
await createTodoResponse;
|
||||
await expect(page.getByText("已从周报生成待办")).toBeVisible();
|
||||
|
||||
const submitResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/weekly-reports/me/submit" && response.ok();
|
||||
});
|
||||
await page.getByTestId(TEST_IDS.weeklyReport.submit).click();
|
||||
await submitResponse;
|
||||
await expect(page.getByText("周报已提交")).toBeVisible();
|
||||
await expect(page.getByText("已提交", { exact: true })).toBeVisible();
|
||||
|
||||
await (await openNavLink(page, "待办")).click();
|
||||
const todoRow = page.getByTestId(TEST_IDS.todo.row).filter({ hasText: uniqueToken }).first();
|
||||
await expect(todoRow).toBeVisible({ timeout: 30_000 });
|
||||
await todoRow.getByTestId(TEST_IDS.todo.complete).click();
|
||||
await expect(todoRow).toContainText("done", { timeout: 30_000 });
|
||||
|
||||
await logout(page, EMPLOYEE.name);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户