import { expect, test, type Locator, type Page } from "@playwright/test"; import { openNavLink } from "./helpers/navigation"; const FINANCE_MGR = { identifier: "finance_mgr@hyinda.com", password: "FinanceMgrPassword1234", name: "钱主管", employeeNo: "HYD000005" }; const ADMINISTRATION = { identifier: "administration@hyinda.com", password: "AdministrationPassword1234", name: "周行政", employeeNo: "HYD000008" }; 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/); } async function selectOptionByText(locator: Locator, pattern: RegExp) { await expect .poll(async () => { return locator.locator("option").evaluateAll((options, rawPattern) => { const regexp = new RegExp(rawPattern as string); return options .map((option) => ({ value: (option as HTMLOptionElement).value, label: (option as HTMLOptionElement).label, disabled: (option as HTMLOptionElement).disabled, })) .filter((option) => option.value && !option.disabled && regexp.test(option.label)) .map((option) => option.value)[0] ?? ""; }, pattern.source); }) .not.toBe(""); const optionValue = await locator.locator("option").evaluateAll((options, rawPattern) => { const regexp = new RegExp(rawPattern as string); return options .map((option) => ({ value: (option as HTMLOptionElement).value, label: (option as HTMLOptionElement).label, disabled: (option as HTMLOptionElement).disabled, })) .filter((option) => option.value && !option.disabled && regexp.test(option.label)) .map((option) => option.value)[0] ?? ""; }, pattern.source); await locator.selectOption(optionValue); } async function selectFirstBusinessOption(locator: Locator) { await expect .poll(async () => { const optionValues = await locator.locator("option").evaluateAll((options) => options .map((option) => ({ value: (option as HTMLOptionElement).value, disabled: (option as HTMLOptionElement).disabled })) .filter((option) => option.value && !option.disabled) .map((option) => option.value), ); return optionValues[0] ?? ""; }) .not.toBe(""); const optionValues = await locator.locator("option").evaluateAll((options) => options .map((option) => ({ value: (option as HTMLOptionElement).value, disabled: (option as HTMLOptionElement).disabled })) .filter((option) => option.value && !option.disabled) .map((option) => option.value), ); await locator.selectOption(optionValues[0]); } test("E2E 资产流转主链路:建卡→领用归还→调拨→盘点", async ({ page }) => { const suffix = Date.now(); const categoryCode = `PW_ASSET_${suffix}`; const categoryName = `Playwright资产分类${suffix}`; const assetName = `Playwright资产卡片${suffix}`; await login(page, FINANCE_MGR); await (await openNavLink(page, "资产")).click(); await expect(page.getByRole("heading", { name: "资产" })).toBeVisible({ timeout: 30_000 }); await expect(page.getByTestId("asset-category-code")).toHaveCount(0); await page.locator(".page-header-shell").getByRole("button", { name: "更多" }).click(); await page.getByRole("menuitem", { name: "资产分类" }).click(); await expect(page.getByTestId("asset-category-manager")).toBeVisible(); await expect(page.getByTestId("asset-category-code")).toBeVisible({ timeout: 30_000 }); await page.getByTestId("asset-category-reset").click(); await page.getByTestId("asset-category-code").fill(categoryCode); await page.getByTestId("asset-category-name").fill(categoryName); await page.getByTestId("asset-category-save").click(); await page.getByTestId("asset-create").click(); await expect(page).toHaveURL(/\/assets\/new$/); await expect(page.getByRole("heading", { name: "新建资产卡片" })).toBeVisible(); await expect(page.getByTestId("asset-name")).toHaveValue(""); await page.getByTestId("asset-name").fill(assetName); await page.getByTestId("asset-category").selectOption({ label: categoryName }); await selectFirstBusinessOption(page.getByTestId("asset-legal-entity")); await selectFirstBusinessOption(page.getByTestId("asset-cost-center")); await page.getByTestId("asset-location").fill("深圳办公室 A-01"); const createAssetResponsePromise = page.waitForResponse( (response) => response.request().method() === "POST" && new URL(response.url()).pathname === "/api/assets", ); await page.getByTestId("asset-save").click(); const createAssetResponse = await createAssetResponsePromise; if (!createAssetResponse.ok()) { const failureText = await createAssetResponse.text(); throw new Error(`asset create failed: ${createAssetResponse.status()} ${failureText}`); } const createAssetPayload = (await createAssetResponse.json()) as { data: { asset: { id: string } }; }; const assetId = createAssetPayload.data.asset.id; await expect(page).toHaveURL(new RegExp(`/assets/${assetId}$`)); await expect(page.getByTestId("unsaved-changes-dialog")).toHaveCount(0); // The URL changes before the saved form state reaches the navigation guard; // going back earlier is blocked as "unsaved" on a loaded host. await expect(page.locator("html")).toHaveAttribute("data-unsaved-changes", "false"); await page.goBack(); await expect(page).toHaveURL(/\/assets$/); await page.goForward(); await expect(page).toHaveURL(new RegExp(`/assets/${assetId}$`)); await page.setViewportSize({ width: 1024, height: 768 }); await expect(page.getByTestId("asset-name")).toHaveValue(assetName, { timeout: 30_000 }); await expect(page.locator(".detail-action-panel")).toHaveCSS("position", "sticky"); const assetRow = page.getByTestId("asset-row").filter({ hasText: assetName }).first(); if (await assetRow.isVisible().catch(() => false)) { await assetRow.click(); } await logout(page, FINANCE_MGR.name); await login(page, ADMINISTRATION); await page.goto(`/assets/${assetId}`); await expect(page.getByTestId("asset-name")).toHaveValue(assetName, { timeout: 30_000 }); await selectOptionByText(page.getByTestId("asset-movement-assignee"), /陈工程/); await page.getByTestId("asset-movement-note").fill("Playwright 首次领用"); if (await page.getByText(/归属人:-/).first().isVisible().catch(() => false)) { await page.getByTestId("asset-assign-submit").click(); } else { await page.getByTestId("asset-transfer-submit").click(); } await expect(page.getByText(/归属人:陈工程/).first()).toBeVisible({ timeout: 30_000 }); await page.getByTestId("asset-return-submit").click(); await expect(page.getByText(/归属人:-/).first()).toBeVisible({ timeout: 30_000 }); await selectOptionByText(page.getByTestId("asset-movement-assignee"), /陈工程/); await page.getByTestId("asset-assign-submit").click(); await expect(page.getByText(/归属人:陈工程/).first()).toBeVisible({ timeout: 30_000 }); await selectOptionByText(page.getByTestId("asset-movement-assignee"), /钱主管/); await page.getByTestId("asset-movement-location").fill("深圳办公室 B-02"); await page.getByTestId("asset-transfer-submit").click(); await expect(page.getByText(/归属人:钱主管/).first()).toBeVisible({ timeout: 30_000 }); await expect(page.getByText(/位置:深圳办公室 B-02/).first()).toBeVisible({ timeout: 30_000 }); await logout(page, ADMINISTRATION.name); await page.setViewportSize({ width: 1440, height: 960 }); await login(page, FINANCE_MGR); await (await openNavLink(page, "资产盘点")).click(); await page.getByTestId("inventory-create").click(); const inventoryRow = page.getByTestId("inventory-row").first(); await expect(inventoryRow).toBeVisible({ timeout: 30_000 }); await page.getByTestId("inventory-start").click(); await selectOptionByText(page.getByTestId("inventory-item-asset"), new RegExp(assetName)); await page.getByTestId("inventory-item-result").selectOption("matched"); await page.getByTestId("inventory-item-note").fill("Playwright 盘点一致"); await page.getByTestId("inventory-item-save").click(); await expect(page.getByRole("row").filter({ hasText: assetName }).last()).toBeVisible({ timeout: 30_000 }); await page.getByTestId("inventory-complete").click(); await expect(page.getByTestId("action-confirm-dialog")).toContainText("确认完成"); await page.getByTestId("action-confirm-submit").click(); await expect(page.locator(".card-std").nth(1).getByText("已完成", { exact: true })).toBeVisible({ timeout: 30_000 }); await logout(page, FINANCE_MGR.name); });