192 行
7.5 KiB
TypeScript
192 行
7.5 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 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");
|
|
});
|