import fs from "node:fs"; import path from "node:path"; import { expect, test, type Locator, type Page } from "@playwright/test"; import { login, type E2EUser } from "../helpers/auth"; type AssetType = "fixed_asset" | "electronic_system" | "code_repository" | "backup_media"; const ASSET_TYPES: Array<{ type: AssetType; label: string; prefix: RegExp; sensitivity: string; externalRef: string; metadata: Record; }> = [ { type: "fixed_asset", label: "FA 固定资产", prefix: /^FA-\d{4}-\d{6}$/, sensitivity: "internal", externalRef: "asset1d-fixed-asset-ledger", metadata: { trialKind: "fixed_asset" }, }, { type: "electronic_system", label: "EA-SYS 电子系统", prefix: /^EA-SYS-\d{4}-\d{6}$/, sensitivity: "internal", externalRef: "https://oa.hyinda.com", metadata: { trialKind: "electronic_system", backupPolicy: "manual-check" }, }, { type: "code_repository", label: "EA-REPO 代码仓库", prefix: /^EA-REPO-\d{4}-\d{6}$/, sensitivity: "internal", externalRef: "git-meta:hyb-oa", metadata: { trialKind: "code_repository", sourceOfTruth: "git" }, }, { type: "backup_media", label: "EA-BKP 备份资产", prefix: /^EA-BKP-\d{4}-\d{6}$/, sensitivity: "confidential", externalRef: "backup-set:ASSET1D", metadata: { trialKind: "backup_media", restoreVerification: "metadata-only" }, }, ]; function requireAdminUser(): E2EUser { const identifier = process.env.LIVE_SMOKE_ADMIN_IDENTIFIER?.trim(); const password = process.env.LIVE_SMOKE_ADMIN_PASSWORD?.trim(); if (!identifier || !password) { throw new Error("missing LIVE_SMOKE_ADMIN_IDENTIFIER/PASSWORD"); } return { identifier, password, name: "OA-only smoke admin" }; } function reportDir() { return path.resolve(process.env.ASSET1D_REPORT_DIR || `../artifacts/qa-flow/asset1d-public-trial-${Date.now()}`); } async function selectFirstBusinessOption(locator: Locator) { await expect .poll(async () => 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)[0] ?? "")) .not.toBe(""); const value = 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)[0] ?? ""); await locator.selectOption(value); } async function fillByPlaceholder(page: Page, placeholder: string | RegExp, value: string) { const field = page.getByPlaceholder(placeholder).first(); await expect(field).toBeVisible(); await field.fill(value); } async function createTrialCategory(page: Page, suffix: string) { const categoryCode = `ASSET1D_${suffix}`; const categoryName = `TEST SMOKE ASSET1D 分类 ${suffix}`; 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 expect(page.getByRole("row").filter({ hasText: categoryCode })).toBeVisible({ timeout: 30_000 }); return { categoryCode, categoryName }; } async function createAsset(page: Page, categoryName: string, suffix: string, index: number, config: (typeof ASSET_TYPES)[number]) { const name = `TEST SMOKE ASSET1D ${config.label} ${suffix}`; await page.getByTestId("asset-create").click(); await expect(page.getByRole("heading", { name: "新建资产卡片" })).toBeVisible(); await page.getByTestId("asset-name").fill(name); await page.getByTestId("asset-type").selectOption(config.type); await page.getByTestId("asset-sensitivity").selectOption(config.sensitivity); await page.getByTestId("asset-category").selectOption({ label: categoryName }); await selectFirstBusinessOption(page.getByTestId("asset-legal-entity")); await selectFirstBusinessOption(page.getByTestId("asset-cost-center")); await selectFirstBusinessOption(page.getByTestId("asset-owner")); await selectFirstBusinessOption(page.getByTestId("asset-keeper")); await page.getByTestId("asset-serial-no").fill(`ASSET1D-${config.type}-${suffix}`); await page.getByTestId("asset-location").fill("TEST-SMOKE-ASSET1D 管理位置"); await page.getByTestId("asset-archive-location").fill(`ASSET1D-ARCHIVE-${config.type}`); await page.getByTestId("asset-external-ref").fill(config.externalRef); await page.getByTestId("asset-source-hash").fill(`asset1d-${suffix}-${index}`.replace(/[^a-z0-9-]/gi, "").slice(0, 64)); await page.getByTestId("asset-verification-no").fill(`VER-${String(Date.now()).slice(-8)}${index}`); await fillByPlaceholder(page, /写回收要求/, "TEST/SMOKE/ASSET1D 试录;不含密钥、源码、完整路径或涉密正文。"); await fillByPlaceholder(page, /不要粘贴 \.env/, JSON.stringify({ testMarker: "TEST-SMOKE-ASSET1D", ...config.metadata, })); const createResponsePromise = page.waitForResponse((response) => { const url = new URL(response.url()); return response.request().method() === "POST" && url.pathname === "/api/assets"; }); await page.getByTestId("asset-save").click(); const createResponse = await createResponsePromise; const payload = await createResponse.json(); if (!createResponse.ok()) { throw new Error(`asset create failed: ${createResponse.status()} ${JSON.stringify(payload).slice(0, 300)}`); } const asset = payload.data?.asset; const assetNo = String(asset?.assetNo || ""); expect(assetNo).toMatch(config.prefix); await page.goto(`/assets?id=${asset.id}`); await expect(page.getByTestId("asset-name")).toHaveValue(name, { timeout: 30_000 }); await page.getByPlaceholder("搜索编号/名称/hash/VER").fill(assetNo); await expect(page.getByTestId("asset-row").filter({ hasText: assetNo })).toBeVisible({ timeout: 30_000 }); return { type: config.type, assetNo, name, id: asset.id, expectedPrefix: config.prefix.source, }; } test("ASSET1D 公网资产试录:FA/EA-SYS/EA-REPO/EA-BKP", async ({ page }) => { test.skip(process.env.ASSET1D_PUBLIC_TRIAL !== "1", "ASSET1D public trial requires explicit ASSET1D_PUBLIC_TRIAL=1"); const admin = requireAdminUser(); const suffix = String(Date.now()).slice(-8); const outputDir = reportDir(); fs.mkdirSync(outputDir, { recursive: true }); await login(page, admin); await page.goto("/assets"); await page.locator(".page-header-shell").getByRole("button", { name: "更多" }).click(); await page.getByRole("menuitem", { name: "资产分类" }).click(); await expect(page.getByTestId("asset-category-code")).toBeVisible({ timeout: 30_000 }); const category = await createTrialCategory(page, suffix); const createdAssets = []; for (const [index, config] of ASSET_TYPES.entries()) { createdAssets.push(await createAsset(page, category.categoryName, suffix, index + 1, config)); } const summary = { issueId: "ASSET1D-4", status: "PASS_ASSET_UI_TRIAL_READY", mode: "asset1d-public-ui-trial", checkedAt: new Date().toISOString(), baseUrl: process.env.E2E_BASE_URL || "https://oa.hyinda.com", category, createdAssets, boundaries: { noEaDocWrite: true, noFileUpload: true, noSecretOutput: true, noSourceBundleUpload: true, noFullPathOutput: true, testDataRetainedForAudit: true, }, }; fs.writeFileSync(path.join(outputDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8"); });