Refine business compare configuration and history UI

This commit is contained in:
oliver 2026-10-10 23:50:06 +08:00
parent 4d5101ec3a
commit 051a10ad95
16 changed files with 1107 additions and 236 deletions

View file

@ -43,6 +43,16 @@ const diffs = [
let failDiffs = false;
let active = false;
let legacy = false;
let mixedHistory = false;
let failHistory = false;
let historyDelay = 0;
const history = [run,
{ ...run, id: "run-loading", status: "running", progress: { phase: "loading", sheet_index: 1, sheet_total: 3, sheet_title: "BGP 邻居", elapsed_ms: 18000 } },
{ ...run, id: "run-queued", status: "queued", progress: { phase: "queued", sheet_total: 0 } },
{ ...run, id: "run-failed", status: "failed", message: "读取原始批次失败,请检查采集状态后重试。" },
{ ...run, id: "run-cancelled", status: "cancelled", message: "操作员已终止本次对比。" },
];
const extraSheet = { ...sheet, sheet_id: "arp", title: "ARP 条目", metric_id: "arp" };
let pollCounts = { runs: 0, detail: 0 };
await page.route("**/v1/**", async (route) => {
const url = new URL(route.request().url());
@ -50,14 +60,16 @@ await page.route("**/v1/**", async (route) => {
let data = {};
if (p === "/v1/auth/me") data = { user: { id: "test", username: "test", role: "admin", is_active: true } };
else if (p.endsWith("/compare/jobs")) data = { items: [job, { ...job, id: "job-slow", name: "慢任务" }] };
else if (p.endsWith("/compare/templates")) data = { items: [{ id: "tpl", name: "割接验收模板", metrics: [sheet] }] };
else if (p.endsWith("/compare/templates")) data = { items: [{ id: "tpl", name: "割接验收模板", metrics: [sheet, extraSheet] }] };
else if (p.endsWith("/compare/metrics")) data = { items: [{ metric_id: "bgp_peer", fields: fields.map((name) => ({ name, display_name: name, dtype: "string", is_key: sheet.key_fields.includes(name) })) }] };
else if (p.endsWith("/tasks")) data = { items: [{ id: "t1", ne_name: "PE-BEFORE", ne_ip: "10.0.0.1" }, { id: "t2", ne_name: "PE-AFTER", ne_ip: "10.0.0.2" }] };
else if (p.endsWith("/batches")) data = { items: [{ id: p.includes("t1") ? "b" : "a", status: "success", row_count: 4 }] };
else if (p.endsWith("/tasks")) data = { items: [{ id: "t1", ne_name: "PE-BEFORE", ne_ip: "10.0.0.1", vendor: "ZTE" }, { id: "t2", ne_name: "PE-AFTER", ne_ip: "10.0.0.2", vendor: "ZTE" }] };
else if (p.endsWith("/batches")) data = { items: [{ id: p.includes("t1") ? "b" : "a", status: "success", row_count: 4, started_at: "2026-10-10T09:00:00Z" }] };
else if (p.endsWith("/runs") && p.includes("/jobs/")) {
pollCounts.runs++;
if (p.includes("job-slow")) await new Promise((resolve) => setTimeout(resolve, 900));
data = { items: [{ ...run, status: active ? "running" : "success" }] };
if (historyDelay) await new Promise((resolve) => setTimeout(resolve, historyDelay));
if (failHistory) { await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ detail: "测试:历史读取暂时中断" }) }); return; }
data = { items: mixedHistory ? history : [{ ...run, status: active ? "running" : "success" }] };
} else if (p.endsWith("/diffs")) {
if (failDiffs) { await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ detail: "测试:连接暂时中断" }) }); return; }
const kind = url.searchParams.get("kind") || "diff";
@ -74,8 +86,44 @@ await page.route("**/v1/**", async (route) => {
try {
await page.goto(`${base}/network/cutover/biz-compare`);
await page.getByRole("row").filter({ hasText: "割接前后 · 业务验收" }).getByText("已就绪", { exact: true }).waitFor();
assert.equal(await page.getByRole("row").filter({ hasText: "割接前后 · 业务验收" }).getByText("手工选批次", { exact: true }).count(), 1);
await page.screenshot({ path: path.join(output, "biz-compare-jobs.png"), fullPage: true });
await page.getByRole("row").filter({ hasText: "割接前后 · 业务验收" }).getByRole("button", { name: "详情", exact: true }).click();
await page.getByRole("button", { name: "比对结果", exact: true }).click();
await page.locator(".bs-cmp-runs__table tbody tr").first().waitFor();
await page.getByRole("tab", { name: "任务配置", exact: true }).click();
await page.getByLabel("操作前批次", { exact: true }).locator('option[value="b"]').waitFor({ state: "attached" });
assert.equal(await page.getByLabel("操作前批次", { exact: true }).inputValue(), "b");
assert.equal(await page.getByLabel("操作后批次", { exact: true }).inputValue(), "a");
assert.equal(await page.locator(".bs-cmp-source").first().getByRole("combobox").count(), 2);
assert.equal(await page.locator(".bs-cmp-source").last().getByRole("combobox").count(), 2);
assert.equal(await page.locator(".bs-cmp-config__section").count(), 4);
await page.screenshot({ path: path.join(output, "biz-compare-config.png"), fullPage: true });
await page.locator(".bs-cmp-scope__item").nth(1).click();
assert.ok(await page.locator(".bs-cmp-scope__item").first().isDisabled(), "last enabled sheet stays locked");
const savedRequest = page.waitForRequest((req) => req.method() === "PATCH" && req.url().endsWith("/compare/jobs/job-1"));
await page.getByRole("button", { name: "保存配置", exact: true }).click();
const body = (await savedRequest).postDataJSON();
assert.deepEqual(body.enabled_sheet_ids, ["peer"]);
assert.equal(body.before_batch_id, "b");
assert.equal(body.after_batch_id, "a");
await page.getByLabel("模式", { exact: true }).selectOption("auto");
assert.equal(await page.getByLabel("操作后批次", { exact: true }).count(), 0);
await page.getByText("保持操作前批次固定;操作后任务采集完成时,自动取最新批次进行对比。", { exact: true }).waitFor();
await page.getByLabel("模式", { exact: true }).selectOption("manual");
await page.setViewportSize({ width: 390, height: 1000 });
assert.ok(await page.locator(".bs-cmp-config").evaluate((el) => el.scrollWidth <= el.clientWidth + 1));
assert.ok(await page.locator(".bs-cmp-job-tabs").evaluate((el) => el.scrollWidth <= el.clientWidth + 1));
await page.screenshot({ path: path.join(output, "biz-compare-config-mobile.png"), fullPage: true });
await page.setViewportSize({ width: 1440, height: 1000 });
await page.getByRole("tab", { name: "任务配置", exact: true }).focus();
await page.keyboard.press("ArrowRight");
assert.equal(await page.getByRole("tab", { name: "对比批次", exact: true }).getAttribute("aria-selected"), "true");
await page.keyboard.press("End");
assert.equal(await page.getByRole("tab", { name: "比对结果", exact: true }).getAttribute("aria-selected"), "true");
await page.getByRole("tab", { name: "比对结果", exact: true }).click();
await page.locator(".bs-cmp-diff-table tbody tr.bs-cmp-row").first().waitFor();
assert.equal(await page.locator(".bs-cmp-diff-table tbody tr.bs-cmp-row").count(), 2);
assert.equal(await page.locator(".bs-cmp-board__config").textContent(), "执行配置已保存");
@ -106,7 +154,7 @@ try {
await page.getByRole("row").filter({ hasText: "慢任务" }).getByRole("button", { name: "详情", exact: true }).click();
await page.getByRole("button", { name: "关闭", exact: true }).click();
await page.getByRole("row").filter({ hasText: "割接前后 · 业务验收" }).getByRole("button", { name: "详情", exact: true }).click();
await page.getByRole("button", { name: "比对结果", exact: true }).click();
await page.getByRole("tab", { name: "比对结果", exact: true }).click();
await page.locator(".bs-cmp-row").first().waitFor();
await page.waitForTimeout(1100);
assert.equal(await page.locator(".bs-cmp-row").count(), 2, "slow old job must not change the current tab");
@ -114,10 +162,53 @@ try {
legacy = true;
await page.reload();
await page.getByRole("row").filter({ hasText: "割接前后 · 业务验收" }).getByRole("button", { name: "详情", exact: true }).click();
await page.getByRole("button", { name: "比对结果", exact: true }).click();
await page.getByRole("tab", { name: "比对结果", exact: true }).click();
await page.getByText("旧记录:未保存执行配置", { exact: true }).waitFor();
await page.screenshot({ path: path.join(output, "biz-compare-legacy.png"), fullPage: true });
mixedHistory = true;
await page.getByRole("tab", { name: "对比批次", exact: true }).click();
await page.locator(".bs-cmp-runs__table tbody tr").nth(4).waitFor();
assert.deepEqual(await page.locator(".bs-cmp-run-stats dd").allTextContents(), ["5", "2", "1", "2"]);
assert.equal(await page.locator('tr[data-status="queued"]').getByRole("progressbar").getAttribute("aria-valuenow"), null);
assert.ok(await page.locator('tr[data-status="running"]').getByRole("button", { name: "查看结果" }).isDisabled());
assert.equal(await page.locator('tr[data-status="success"]').getByText("已完成", { exact: true }).count(), 1);
assert.equal(await page.locator('tr[data-status="success"] .bs-cmp-run-verdict .is-fail strong').textContent(), "2");
await page.setViewportSize({ width: 1440, height: 1000 });
await page.screenshot({ path: path.join(output, "biz-compare-runs.png"), fullPage: true });
await page.setViewportSize({ width: 390, height: 1000 });
assert.ok(await page.locator(".bs-cmp-runs").evaluate((el) => el.scrollWidth <= el.clientWidth + 1));
await page.screenshot({ path: path.join(output, "biz-compare-runs-mobile.png"), fullPage: true });
await page.setViewportSize({ width: 768, height: 1000 });
historyDelay = 1000;
await page.getByRole("button", { name: "刷新记录", exact: true }).click();
assert.ok(await page.getByRole("button", { name: "刷新记录", exact: true }).isDisabled());
await page.waitForFunction(() => document.querySelector(".bs-cmp-runs")?.getAttribute("aria-busy") === "false");
historyDelay = 0;
failHistory = true;
await page.getByRole("button", { name: "刷新记录", exact: true }).click();
await page.locator(".bs-cmp-runs__error").waitFor();
assert.equal(await page.locator(".bs-cmp-runs__table tbody tr").count(), 5);
failHistory = false;
await page.getByRole("button", { name: "重试", exact: true }).click();
await page.locator(".bs-cmp-runs__error").waitFor({ state: "detached" });
mixedHistory = false;
await page.getByRole("button", { name: "关闭", exact: true }).click();
await page.getByRole("button", { name: "创建比对任务", exact: true }).click();
await page.getByRole("button", { name: "下一步", exact: true }).click();
await page.getByRole("button", { name: "下一步", exact: true }).click();
await page.getByLabel("操作前任务", { exact: true }).selectOption("t1");
await page.getByLabel("操作后批次", { exact: true }).locator('option[value="b"]').waitFor({ state: "attached" });
await page.getByLabel("操作后批次", { exact: true }).selectOption("b");
await page.getByLabel("操作后任务", { exact: true }).selectOption("t2");
assert.equal(await page.getByLabel("操作后批次", { exact: true }).inputValue(), "", "changing source clears the previous batch");
await page.getByLabel("操作前批次", { exact: true }).selectOption("b");
await page.getByLabel("操作后批次", { exact: true }).selectOption("a");
await page.getByRole("button", { name: "下一步", exact: true }).click();
await page.getByLabel("接口对应关系", { exact: true }).waitFor();
await page.getByRole("button", { name: "关闭", exact: true }).click();
active = true;
await page.reload();
await page.getByRole("row").filter({ hasText: "割接前后 · 业务验收" }).getByRole("button", { name: "详情", exact: true }).click();
@ -127,7 +218,11 @@ try {
assert.ok(pollCounts.runs <= 3 && pollCounts.detail <= 3, JSON.stringify(pollCounts));
assert.ok(pollCounts.runs >= 2 && pollCounts.detail >= 2, "active run should keep polling");
assert.deepEqual(errors, []);
console.log(JSON.stringify({ passed: ["result layout", "saved execution settings", "legacy settings warning", "error clears stale rows", "retry", "clear filters", "768px toolbar", "old job response ignored", "single poller", "no runtime errors"], pollCounts, output }));
console.log(JSON.stringify({ passed: ["job list labels", "four config sections", "source pairing", "last sheet locked", "configuration save payload", "automatic mode", "390px configuration", "keyboard tabs", "wizard source selection", "same-task batch selection", "source change clears batch", "history counts", "execution and verdict separation", "queued progress semantics", "history loading", "history error retry", "390px history", "result layout", "saved execution settings", "legacy settings warning", "error clears stale rows", "retry", "clear filters", "768px toolbar", "old job response ignored", "single poller", "no runtime errors"], pollCounts, output }));
} catch (error) {
await page.screenshot({ path: path.join(output, "biz-compare-test-failure.png"), fullPage: true });
console.error(JSON.stringify(await page.locator(".bs-cmp-source select").evaluateAll((els) => els.map((el) => ({ value: el.value, html: el.parentElement.outerHTML })))));
throw error;
} finally {
await browser.close();
}