feat(cutover): add live fullscreen monitoring wall

This commit is contained in:
oliver 2026-10-11 09:08:03 +08:00
parent b9d5e27cc4
commit 4d60601050
8 changed files with 307 additions and 10 deletions

View file

@ -12,7 +12,7 @@ const browser = await chromium.launch({ headless: true, channel: process.env.NET
const page = await browser.newPage({ viewport: { width: 1440, height: 1080 } });
const errors = [], passed = [], requests = [], writes = [];
page.on("pageerror", (e) => errors.push(e.message));
let liveRunId = "live-1", uncertain = false, failBoard = false, delayedSheet = "";
let liveRunId = "live-1", uncertain = false, failBoard = false, delayedSheet = "", sampleAgeSec = 0;
const now = new Date().toISOString();
const project = { id: "p", name: "核心 PE 替换", old_task_id: "op", new_task_id: "np", old_hf_task_id: "oh", new_hf_task_id: "nh", old_baseline_batch_id: "ob", new_baseline_batch_id: "nb", monitor_template_id: "mt", hf_interval_sec: 60, status: "active", created_at: now,
collect_metric_ids_effective: ["arp"], old_task: { id: "op", ne_name: "PE-OLD", ne_ip: "10.0.0.1" }, new_task: { id: "np", ne_name: "PE-NEW", ne_ip: "10.0.0.2" } };
@ -22,9 +22,10 @@ const sheets = ["s1", "s2"].map((sid, i) => ({ sheet_id: sid, metric_id: "arp",
const monitor = { id: "mt", name: "业务连续性", compare_template_id: "ct", compare_template_name: "业务模板", collect_metric_ids: ["arp"], collect_metric_ids_effective: ["arp"], available_metric_ids: ["arp", "ip_route"], defaults: { custom_setting: false, threshold: 7, out_of_expect: "ignore" }, sheet_overrides: [{ metric_id: "arp", success: [{ old_groups: [["removed"]], new_groups: [["added"]], name: "客户迁移规则" }], anomaly: [] }] };
const dataset = Array.from({ length: 601 }, (_, i) => ({ id: `row-${i}`, metric_id: "arp", sheet_id: i % 2 ? "s2" : "s1", verdict: i === 600 ? "anomaly" : "migrated", color: i === 600 ? "red" : "green", in_expect: i !== 600, old_key: `10.1.${Math.floor(i / 255)}.${i % 255}`, new_key: `10.1.${Math.floor(i / 255)}.${i % 255}`, old_status: "gone", new_status: "up", rule_hit: i === 600 ? "service_content_changed" : "success#1", integrity_changes: i === 600 ? [{ field: "mac", before: "aa:bb", after: "cc:dd" }] : [] }));
function run(rid, pid = "p") {
const sampleTime = sampleAgeSec ? new Date(Date.now() - sampleAgeSec * 1000).toISOString() : now;
return { id: rid, purpose: rid === "hist" ? "manual" : "auto", created_at: now,
summary: { progress: { ok: pid === "p2" ? 3 : 99, total: pid === "p2" ? 3 : 101 }, anomaly: 1, anomaly_outside_expect: 1, coverage_complete: !uncertain, duplicate_keys: uncertain ? 2 : 0,
sheet_cards: sheets.map((s) => ({ ...s, progress_ok: 49, progress_total: 50, anomaly: 0, old_collected_at: now, new_collected_at: now, age_limit_sec: 180, sample_issues: uncertain ? ["sample_skew"] : [] })) } };
sheet_cards: sheets.map((s) => ({ ...s, progress_ok: 49, progress_total: 50, anomaly: 0, old_collected_at: sampleTime, new_collected_at: sampleTime, age_limit_sec: 180, sample_issues: uncertain ? ["sample_skew"] : [] })) } };
}
await page.route("**/v1/**", async (route) => {
const req = route.request(), url = new URL(req.url()), p = url.pathname;
@ -76,6 +77,66 @@ try {
await dialog().getByText("双端关联采集 · 及时性", { exact: true }).waitFor();
assert.equal(await dialog().locator('.bm-sampling tbody td:last-child').textContent(), '0.4 s');
passed.push("measured paired start skew", "paired round and duration visible", "live continuity KPIs", "outside anomaly visible before acceptance", "100 row page bound");
await page.setViewportSize({ width: 1920, height: 1080 });
await dialog().getByRole("button", { name: "大屏模式", exact: true }).click();
await page.locator('.bm-board.is-wall').waitFor();
assert.ok(await page.evaluate(() => document.fullscreenElement?.classList.contains('bm-board')));
assert.deepEqual(await page.locator('.bm-board').evaluate((el) => [Math.round(el.getBoundingClientRect().width), Math.round(el.getBoundingClientRect().height)]), [1920, 1080]);
await page.locator('.bm-wall-heading').getByRole('heading', { name: project.name, exact: true }).waitFor();
assert.equal(await page.getByRole('progressbar').getAttribute('aria-valuenow'), '98');
await page.locator('.bm-wall-state').getByText('存在业务异常', { exact: true }).waitFor();
assert.equal(await table().locator('tbody tr').count(), 100);
assert.ok(await page.locator('.bm-board').evaluate((el) => el.scrollWidth <= el.clientWidth + 1));
await page.screenshot({ path: path.join(output, 'cutover-monitor-wall.png') });
await page.setViewportSize({ width: 3840, height: 2160 });
assert.deepEqual(await page.locator('.bm-board').evaluate((el) => [Math.round(el.getBoundingClientRect().width), Math.round(el.getBoundingClientRect().height)]), [3840, 2160]);
assert.ok(await page.locator('.bm-board').evaluate((el) => el.scrollWidth <= el.clientWidth + 1));
await page.setViewportSize({ width: 1920, height: 1080 });
const wallRequests = requests.filter((r) => r.p.endsWith('/board')).length;
await page.waitForTimeout(10500);
assert.ok(requests.filter((r) => r.p.endsWith('/board')).length > wallRequests);
await page.getByRole('button', { name: '退出大屏', exact: true }).click();
await page.waitForFunction(() => !document.querySelector('.bm-board.is-wall') && !document.fullscreenElement);
assert.ok(await dialog().isVisible());
assert.ok(await dialog().getByRole('checkbox', { name: '仅预期', exact: true }).isChecked());
passed.push('native wall fills 1920x1080', '4K wall fits without horizontal overflow', 'project identity and progress visible on wall', 'outside alarms stay visible on wall', 'wall keeps one bounded data page', 'live wall keeps refreshing', 'wall exit preserves project and filters');
// Block native fullscreen to verify the window layout and Escape handling.
await page.evaluate(() => {
window.__originalFullscreen = Element.prototype.requestFullscreen;
Element.prototype.requestFullscreen = async () => { throw new Error('fixture_fullscreen_blocked'); };
});
await dialog().getByRole('button', { name: '大屏模式', exact: true }).click();
await page.locator('.bm-board.is-wall').waitFor();
assert.equal(await page.evaluate(() => document.fullscreenElement), null);
assert.deepEqual(await page.locator('.bm-board').evaluate((el) => [Math.round(el.getBoundingClientRect().width), Math.round(el.getBoundingClientRect().height)]), [1920, 1080]);
uncertain = true;
await page.getByRole('button', { name: '刷新看板', exact: true }).click();
await page.locator('.bm-wall-state').getByText('数据待确认', { exact: true }).waitFor();
uncertain = false; sampleAgeSec = 200;
await page.getByRole('button', { name: '刷新看板', exact: true }).click();
await page.getByText('采集已过期', { exact: true }).waitFor();
assert.equal(await page.locator('.bm-wall-state').textContent(), '数据待确认');
sampleAgeSec = 0;
failBoard = true;
await page.getByRole('button', { name: '刷新看板', exact: true }).click();
await dialog().getByRole('alert').filter({ hasText: '获取失败' }).waitFor();
assert.equal(await page.locator('.bm-wall-state').textContent(), '数据待确认');
const clockText = await page.locator('.bm-wall-heading time').textContent();
await page.waitForFunction((previous) => document.querySelector('.bm-wall-heading time')?.textContent !== previous,
clockText, { timeout: 5000 });
assert.notEqual(await page.locator('.bm-wall-heading time').textContent(), clockText);
failBoard = false;
await page.setViewportSize({ width: 390, height: 1000 });
assert.ok(await page.locator('.bm-board').evaluate((el) => el.scrollWidth <= el.clientWidth + 1));
await page.keyboard.press('Escape');
await page.waitForFunction(() => !document.querySelector('.bm-board.is-wall'));
assert.ok(await dialog().isVisible());
assert.ok(await dialog().getByRole('checkbox', { name: '仅预期', exact: true }).isChecked());
await page.evaluate(() => { Element.prototype.requestFullscreen = window.__originalFullscreen; });
await page.setViewportSize({ width: 1440, height: 1080 });
await dialog().getByRole('button', { name: '刷新看板', exact: true }).click();
await page.waitForFunction(() => !document.querySelector('.bm-live-warning[role="alert"]'));
passed.push('blocked fullscreen uses full-window wall', 'unfinished pair cannot look healthy on wall', 'stale samples remain uncertain on wall', 'refresh failure retains explicit uncertainty', 'wall clock continues when refresh fails', '390px wall stays within viewport', 'Escape exits wall without closing project');
await dialog().getByRole("button", { name: "下一页", exact: true }).click();
await page.waitForFunction(() => document.querySelector('.bm-board__table tbody tr code')?.textContent === '10.1.0.100');
passed.push("server pagination beyond first page");
@ -110,15 +171,24 @@ try {
uncertain = false;
await dialog().getByRole("combobox", { name: "判定历史", exact: true }).selectOption("hist");
await dialog().getByText("历史快照", { exact: true }).waitFor();
await dialog().getByRole('button', { name: '大屏模式', exact: true }).click();
await page.locator('.bm-board.is-wall').waitFor();
await page.getByText('历史快照', { exact: true }).waitFor();
const historyBoardRequests = requests.filter((r) => r.p.endsWith("/board")).length;
await page.waitForTimeout(10500);
assert.equal(requests.filter((r) => r.p.endsWith("/board")).length, historyBoardRequests);
passed.push("historical view remains frozen");
await page.keyboard.press('Escape');
await page.waitForFunction(() => !document.querySelector('.bm-board.is-wall') && !document.fullscreenElement);
assert.ok(await dialog().isVisible());
assert.equal(await dialog().getByRole('combobox', { name: '判定历史', exact: true }).inputValue(), 'hist');
passed.push("historical wall remains frozen", "native Escape preserves project and historical selection");
liveRunId = "live-2";
const liveResponse = page.waitForResponse((r) => r.url().includes('/runs/live-2/diffs'));
await dialog().getByRole("combobox", { name: "判定历史", exact: true }).selectOption("");
await liveResponse;
await waitRows(100);
const liveCount = requests.filter((r) => r.p.endsWith("/board")).length;
await page.waitForTimeout(10500);
await page.waitForResponse((r) => new URL(r.url()).pathname.endsWith('/board'), { timeout: 15000 });
assert.ok(requests.filter((r) => r.p.endsWith("/board")).length > liveCount);
passed.push("active live board automatically refreshes", "refresh cycles do not overlap");
failBoard = true;
@ -174,6 +244,15 @@ try {
console.log(JSON.stringify({ passed, errors }, null, 2));
} catch (error) {
await page.screenshot({ path: path.join(output, "cutover-monitor-failure.png"), fullPage: true });
console.log(JSON.stringify(await page.locator('.bm-board').evaluate((el) => {
const layout = [];
for (let node = el; node; node = node.parentElement) {
const rect = node.getBoundingClientRect(), style = getComputedStyle(node);
layout.push({ class: node.className, width: rect.width, height: rect.height, x: rect.x, y: rect.y,
minWidth: style.minWidth, maxWidth: style.maxWidth, padding: style.padding, margin: style.margin, transform: style.transform });
}
return layout;
}), null, 2));
console.log(JSON.stringify({ passed, errors, recentRequests: requests.slice(-12) }, null, 2));
throw error;
} finally {