From 347979252452543c1c88c485b7482fbc0c87126a Mon Sep 17 00:00:00 2001 From: shunli Date: Sun, 22 Mar 2026 11:31:34 +0800 Subject: [PATCH] =?UTF-8?q?R23-5=EF=BC=9A=E8=A1=A5=E9=BD=90=E5=85=B3?= =?UTF-8?q?=E9=94=AEE2E=E5=B9=B6=E4=BF=AE=E5=A4=8D=E8=AF=95=E7=82=B9?= =?UTF-8?q?=E9=AA=8C=E6=94=B6=E7=A8=B3=E5=AE=9A=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/e2e/confidential-boundary.spec.ts | 114 ++++++++++++++++++ client/tests/e2e/procurement-flow.spec.ts | 4 +- client/tests/e2e/project-workbench.spec.ts | 106 ++++++++++++++++ client/tests/e2e/wecom-entry.spec.ts | 40 ++++++ 4 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 client/tests/e2e/confidential-boundary.spec.ts create mode 100644 client/tests/e2e/project-workbench.spec.ts create mode 100644 client/tests/e2e/wecom-entry.spec.ts diff --git a/client/tests/e2e/confidential-boundary.spec.ts b/client/tests/e2e/confidential-boundary.spec.ts new file mode 100644 index 00000000..c448a0cd --- /dev/null +++ b/client/tests/e2e/confidential-boundary.spec.ts @@ -0,0 +1,114 @@ +import { expect, test, type APIRequestContext } from "@playwright/test"; + +import { apiGetData, apiPostData, createAuthenticatedApiClient, login } from "./helpers/auth"; + +const ADMIN = { + identifier: "admin@hyinda.com", + password: "AdminPassword1234", + name: "Admin", +}; + +type FeatureFlagItem = { + flagKey: string; + overrideMode: "inherit" | "force_on" | "force_off"; + rolloutType: "global" | "department_allowlist"; + departmentIds: string[]; + note: string; + version: number; + effectiveEnabled: boolean; +}; + +async function readApiData(response: Awaited>) { + const payload = (await response.json()) as { data?: T; error?: { code?: string; message?: string } }; + if (!response.ok()) { + throw new Error(`${payload.error?.code ?? response.status()}: ${payload.error?.message ?? "request_failed"}`); + } + return payload.data as T; +} + +async function ensureFeatureFlags(api: APIRequestContext, keys: string[]) { + const list = await apiGetData<{ items: FeatureFlagItem[] }>(api, "/api/system/feature-flags"); + for (const key of keys) { + const item = list.items.find((flag) => flag.flagKey === key); + if (!item) { + throw new Error(`Missing feature flag: ${key}`); + } + if (item.overrideMode === "force_on" && item.effectiveEnabled) { + continue; + } + await readApiData<{ item: FeatureFlagItem }>( + await api.put(`/api/system/feature-flags/${key}`, { + data: { + overrideMode: "force_on", + rolloutType: "global", + departmentIds: [], + note: "playwright e2e", + expectedVersion: item.version, + }, + }), + ); + } +} + +test("E2E 涉密边界:敏感摘要不可搜索,编号可命中", async ({ page }) => { + const adminClient = await createAuthenticatedApiClient(ADMIN); + const suffix = Date.now(); + const sensitivePhrase = `极密摘要${suffix}`; + const contractNo = `HT-${suffix}`; + const docNo = `DOC-${suffix}`; + + await ensureFeatureFlags(adminClient.api, [ + "global_search_v1", + "project_domain_v1", + "project_linking_v1", + "confidential_doc_refs_v1", + "public_security_hardening_v1", + ]); + + const project = await apiPostData<{ item: { id: string; name: string } }>(adminClient.api, "/api/projects", { + name: `涉密项目 ${suffix}`, + clientName: `涉密客户 ${suffix}`, + summary: "用于 R23 涉密边界 E2E", + isConfidential: true, + }); + + const contract = await apiPostData<{ item: { id: string } }>(adminClient.api, "/api/contracts-registry", { + contractNo, + projectId: project.item.id, + counterpartyName: `客户 ${suffix}`, + summary: sensitivePhrase, + confidentialityLevel: "restricted", + archiveLocation: `NAS/Contracts/${suffix}`, + offlineArchiveNo: `ARC-${suffix}`, + }); + + await apiPostData<{ item: { id: string } }>(adminClient.api, "/api/confidential-doc-refs", { + projectId: project.item.id, + contractRegistryId: contract.item.id, + title: `资料引用 ${suffix}`, + documentNo: docNo, + confidentialityLevel: "confidential", + storageType: "offline_archive", + storagePath: `OFFLINE/${suffix}`, + offlineArchiveNo: `DOC-ARC-${suffix}`, + }); + + await login(page, ADMIN); + await page.goto(`/projects/${project.item.id}`); + await expect(page.getByText(sensitivePhrase)).toHaveCount(0); + + await page.getByTestId("global-search-trigger").click(); + const searchInput = page.getByPlaceholder("搜索人员、项目、合同、公告、资产、请假、报销、周报、待办"); + + await searchInput.fill(sensitivePhrase); + await expect(page.getByRole("button", { name: new RegExp(contractNo) })).toHaveCount(0); + await expect(page.getByRole("button", { name: new RegExp(docNo) })).toHaveCount(0); + + await searchInput.fill(contractNo); + await expect(page.getByRole("button", { name: new RegExp(contractNo) })).toBeVisible(); + + await searchInput.fill(docNo); + await expect(page.getByRole("button", { name: new RegExp(docNo) })).toBeVisible(); + + await adminClient.dispose(); +}); diff --git a/client/tests/e2e/procurement-flow.spec.ts b/client/tests/e2e/procurement-flow.spec.ts index 424b95c3..925ffcbe 100644 --- a/client/tests/e2e/procurement-flow.spec.ts +++ b/client/tests/e2e/procurement-flow.spec.ts @@ -45,7 +45,7 @@ test("E2E 采购主链路:员工提单→经理审批→财务归档", async ( const title = `Playwright 采购 ${Date.now()}`; await login(page, EMPLOYEE); - await page.getByRole("link", { name: "采购申请" }).click(); + await page.getByRole("link", { name: "采购申请", exact: true }).first().click(); await page.getByTestId("purchase-requests-create").click(); await expect(page.getByTestId("purchase-title")).toBeVisible(); @@ -85,7 +85,7 @@ test("E2E 采购主链路:员工提单→经理审批→财务归档", async ( await logout(page, MANAGER.name); await login(page, FINANCE_MGR); - await page.getByRole("link", { name: "采购申请" }).click(); + await page.getByRole("link", { name: "采购申请", exact: true }).first().click(); await page.getByRole("button", { name: "全范围" }).click(); await page.getByRole("combobox").first().selectOption("approved"); const purchaseRow = page.getByRole("row").filter({ hasText: title }).first(); diff --git a/client/tests/e2e/project-workbench.spec.ts b/client/tests/e2e/project-workbench.spec.ts new file mode 100644 index 00000000..9d5856af --- /dev/null +++ b/client/tests/e2e/project-workbench.spec.ts @@ -0,0 +1,106 @@ +import { expect, test, type APIRequestContext } from "@playwright/test"; + +import { apiGetData, apiPostData, createAuthenticatedApiClient, login } from "./helpers/auth"; + +const ADMIN = { + identifier: "admin@hyinda.com", + password: "AdminPassword1234", + name: "Admin", +}; + +type FeatureFlagItem = { + flagKey: string; + overrideMode: "inherit" | "force_on" | "force_off"; + rolloutType: "global" | "department_allowlist"; + departmentIds: string[]; + note: string; + version: number; + effectiveEnabled: boolean; +}; + +async function readApiData(response: Awaited>) { + const payload = (await response.json()) as { data?: T; error?: { code?: string; message?: string } }; + if (!response.ok()) { + throw new Error(`${payload.error?.code ?? response.status()}: ${payload.error?.message ?? "request_failed"}`); + } + return payload.data as T; +} + +async function ensureFeatureFlags(api: APIRequestContext, keys: string[]) { + const list = await apiGetData<{ items: FeatureFlagItem[] }>(api, "/api/system/feature-flags"); + for (const key of keys) { + const item = list.items.find((flag) => flag.flagKey === key); + if (!item) { + throw new Error(`Missing feature flag: ${key}`); + } + if (item.overrideMode === "force_on" && item.effectiveEnabled) { + continue; + } + await readApiData<{ item: FeatureFlagItem }>( + await api.put(`/api/system/feature-flags/${key}`, { + data: { + overrideMode: "force_on", + rolloutType: "global", + departmentIds: [], + note: "playwright e2e", + expectedVersion: item.version, + }, + }), + ); + } +} + +test("E2E 项目工作台:沟通回填 + 收口面板 + Portal 摘要", async ({ page }) => { + const adminClient = await createAuthenticatedApiClient(ADMIN); + const suffix = Date.now(); + const today = new Date().toISOString().slice(0, 10); + + await ensureFeatureFlags(adminClient.api, [ + "portal_home_v1", + "project_domain_v1", + "project_linking_v1", + "project_communication_log_v1", + "project_closure_checklist_v1", + "confidential_doc_refs_v1", + ]); + + const created = await apiPostData<{ item: { id: string; projectNo: string; name: string } }>(adminClient.api, "/api/projects", { + name: `Playwright 项目工作台 ${suffix}`, + clientName: `客户 ${suffix}`, + summary: "用于验证 R23 项目工作台", + }); + const projectId = created.item.id; + const projectName = created.item.name; + const communicationSummary = `客户确认现场启动安排 ${suffix}`; + const deliveryTitle = `阶段交付 ${suffix}`; + const deliverySummary = `已完成现场启动与交付确认 ${suffix}`; + + await apiPostData<{ item: { id: string } }>(adminClient.api, `/api/projects/${projectId}/deliveries`, { + title: deliveryTitle, + recordType: "onsite", + summary: deliverySummary, + occurredAt: `${today}T00:00:00.000Z`, + }); + + await login(page, ADMIN); + await page.goto(`/projects/${projectId}`); + await expect(page.getByRole("heading", { name: new RegExp(projectName) })).toBeVisible(); + await expect(page.getByTestId("project-communication-panel")).toBeVisible(); + await expect(page.getByTestId("project-closure-panel")).toBeVisible(); + + const communicationPanel = page.getByTestId("project-communication-panel"); + await communicationPanel.getByTestId("project-communication-summary-input").fill(communicationSummary); + await communicationPanel.locator('input[type="date"]').nth(1).fill(today); + await communicationPanel.getByTestId("project-communication-submit").click(); + await expect(communicationPanel.getByText(communicationSummary)).toBeVisible(); + + await expect(page.getByText(deliveryTitle)).toBeVisible(); + + await expect(page.getByTestId("project-closure-panel")).toContainText("确认交付/验收记录齐全"); + + await page.goto("/portal"); + await expect(page.getByText(/项目摘要:/)).toContainText("今日待跟进"); + await expect(page.getByText(/项目摘要:/)).toContainText("待收口项目"); + + await adminClient.dispose(); +}); diff --git a/client/tests/e2e/wecom-entry.spec.ts b/client/tests/e2e/wecom-entry.spec.ts new file mode 100644 index 00000000..45b543f2 --- /dev/null +++ b/client/tests/e2e/wecom-entry.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from "@playwright/test"; + +import { EMPLOYEE, login } from "./helpers/auth"; + +test("E2E 企微入口:登录页展示入口并保留账号密码回退", async ({ page }) => { + await page.route("**/api/auth/wecom/config", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + requestId: "mock-wecom-config", + data: { + enabled: true, + corpId: "wx-test-corp", + agentId: "1000001", + oauthEnabled: true, + loginUrl: "/api/auth/wecom/login?returnTo=%2Fportal", + }, + }), + }); + }); + await page.route("**/api/auth/wecom/login?**", async (route) => { + await route.fulfill({ + status: 200, + contentType: "text/plain", + body: "mock wecom login entry", + }); + }); + + await page.goto("/login"); + await expect(page.getByRole("button", { name: "使用企业微信进入工作台" })).toBeVisible(); + + await page.getByRole("button", { name: "使用企业微信进入工作台" }).click(); + await expect(page).toHaveURL(/\/api\/auth\/wecom\/login\?returnTo=/); + + await login(page, EMPLOYEE); + await page.goto("/settings/profile"); + await expect(page.getByText("企业微信绑定")).toBeVisible(); + await expect(page.getByText(/接入状态:/)).toBeVisible(); +});