文件
hyb-oa/client/tests/e2e/manual/asset1d-public-trial.spec.ts
T

193 行
7.6 KiB
TypeScript

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<string, string>;
}> = [
{
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.getByRole("button", { 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");
});