Unify the Agent history extension with the header acrylic surface, preserve the full conversation body, and consolidate shared control and status styling. Restore map flow and SCADA controller behavior, remove obsolete rendering paths, and extend regression coverage. Button press coverage now releases outside the target so state assertions cannot accidentally toggle the control.
306 lines
12 KiB
TypeScript
306 lines
12 KiB
TypeScript
import { expect, test, type Locator } from "@playwright/test";
|
|
|
|
test("scheduled conditions keep acrylic blur outside transformed ancestors", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
|
|
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
await expect(panel).toBeVisible();
|
|
|
|
await expectAcrylicComposition(panel);
|
|
await expect(panel).toHaveCSS("width", "432px");
|
|
const timeline = panel.getByTestId("scheduled-condition-timeline");
|
|
const detailViewport = panel.locator(".scheduled-feed-detail-viewport");
|
|
const detailScroll = panel.getByTestId("scheduled-condition-detail-scroll");
|
|
const collapsedPanelBox = await panel.boundingBox();
|
|
const collapsedTimelineBox = await timeline.boundingBox();
|
|
expect(collapsedPanelBox).not.toBeNull();
|
|
expect(collapsedTimelineBox).not.toBeNull();
|
|
|
|
await panel.getByRole("button", { name: "展开工况任务" }).click();
|
|
|
|
await expectAcrylicComposition(panel);
|
|
await expect(panel).toHaveCSS("width", "880px");
|
|
const expandedPanelBox = await panel.boundingBox();
|
|
const expandedTimelineBox = await timeline.boundingBox();
|
|
const expandedDetailViewportBox = await detailViewport.boundingBox();
|
|
const expandedDetailScrollBox = await detailScroll.boundingBox();
|
|
expect(expandedPanelBox).not.toBeNull();
|
|
expect(expandedTimelineBox).not.toBeNull();
|
|
expect(expandedDetailViewportBox).not.toBeNull();
|
|
expect(expandedDetailScrollBox).not.toBeNull();
|
|
expect(Math.round(expandedPanelBox!.x + expandedPanelBox!.width)).toBe(
|
|
Math.round(collapsedPanelBox!.x + collapsedPanelBox!.width)
|
|
);
|
|
expect(Math.round(expandedTimelineBox!.width)).toBe(
|
|
Math.round(collapsedTimelineBox!.width)
|
|
);
|
|
expect(Math.round(expandedTimelineBox!.x + expandedTimelineBox!.width)).toBe(
|
|
Math.round(collapsedTimelineBox!.x + collapsedTimelineBox!.width)
|
|
);
|
|
expect(expandedTimelineBox!.y + expandedTimelineBox!.height).toBeLessThanOrEqual(
|
|
expandedPanelBox!.y + expandedPanelBox!.height - 12
|
|
);
|
|
expect(Math.round(expandedDetailViewportBox!.y + expandedDetailViewportBox!.height)).toBe(
|
|
Math.round(expandedTimelineBox!.y + expandedTimelineBox!.height)
|
|
);
|
|
expect(expandedDetailScrollBox!.y).toBeGreaterThanOrEqual(
|
|
expandedDetailViewportBox!.y + 8
|
|
);
|
|
expect(expandedDetailScrollBox!.y + expandedDetailScrollBox!.height).toBeLessThanOrEqual(
|
|
expandedDetailViewportBox!.y + expandedDetailViewportBox!.height - 8
|
|
);
|
|
await expect(detailViewport).toHaveCSS("border-radius", "12px");
|
|
const timelineScroll = timeline.locator(".scheduled-feed-scroll.flex-1");
|
|
expect(await readScrollbarPresentation(detailScroll)).toEqual(
|
|
await readScrollbarPresentation(timelineScroll)
|
|
);
|
|
const futureScrollBox = await timeline.locator(".scheduled-feed-scroll").first().boundingBox();
|
|
const recentScrollBox = await timelineScroll.boundingBox();
|
|
expect(futureScrollBox).not.toBeNull();
|
|
expect(recentScrollBox).not.toBeNull();
|
|
expect(Math.round(futureScrollBox!.x + futureScrollBox!.width)).toBe(
|
|
Math.round(recentScrollBox!.x + recentScrollBox!.width)
|
|
);
|
|
|
|
await panel.getByRole("button", { name: "收起工况任务" }).click();
|
|
await expect(panel).toHaveCSS("width", "432px");
|
|
const restoredTimelineBox = await timeline.boundingBox();
|
|
expect(restoredTimelineBox).not.toBeNull();
|
|
expect(Math.round(restoredTimelineBox!.width)).toBe(
|
|
Math.round(collapsedTimelineBox!.width)
|
|
);
|
|
});
|
|
|
|
test("narrow desktop keeps Agent and expanded conditions from overlapping", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1024, height: 768 });
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
|
|
|
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
|
|
const resizeHandle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
|
|
await resizeHandle.focus();
|
|
await page.keyboard.press("End");
|
|
await expect(agentPanel).toHaveCSS("width", "484px");
|
|
|
|
const compactAgentBox = await agentPanel.boundingBox();
|
|
const compactConditionBox = await panel.boundingBox();
|
|
expect(compactAgentBox).not.toBeNull();
|
|
expect(compactConditionBox).not.toBeNull();
|
|
expect(compactConditionBox!.x - (compactAgentBox!.x + compactAgentBox!.width)).toBeGreaterThanOrEqual(
|
|
24
|
|
);
|
|
|
|
await panel.getByRole("button", { name: "展开工况任务" }).click();
|
|
await expect(page.locator('aside[aria-label="Agent 折叠栏"]')).toBeVisible();
|
|
await expect(panel).toHaveCSS("width", "852px");
|
|
|
|
const expandedConditionBox = await panel.boundingBox();
|
|
const collapsedAgentBox = await page
|
|
.locator('aside[aria-label="Agent 折叠栏"]')
|
|
.boundingBox();
|
|
expect(expandedConditionBox).not.toBeNull();
|
|
expect(collapsedAgentBox).not.toBeNull();
|
|
expect(
|
|
expandedConditionBox!.x - (collapsedAgentBox!.x + collapsedAgentBox!.width)
|
|
).toBeGreaterThanOrEqual(24);
|
|
|
|
await panel.getByRole("button", { name: "收起工况任务" }).click();
|
|
await expect(agentPanel).toBeVisible();
|
|
});
|
|
|
|
test("switching conditions keeps the detail viewport fully expanded", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
|
|
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
await panel.getByRole("button", { name: "展开工况任务" }).click();
|
|
await expect(panel).toHaveCSS("width", "880px");
|
|
|
|
const detailScroll = panel.getByTestId("scheduled-condition-detail-scroll");
|
|
const baselineHeight = await detailScroll.evaluate(
|
|
(element) => element.getBoundingClientRect().height
|
|
);
|
|
const targetCondition = panel
|
|
.getByTestId("scheduled-condition-timeline")
|
|
.getByRole("button", { name: /SCADA 数据诊断/ })
|
|
.nth(1);
|
|
const sampledHeights = await targetCondition.evaluate(async (element) => {
|
|
element.click();
|
|
const heights: number[] = [];
|
|
const startedAt = performance.now();
|
|
|
|
await new Promise<void>((resolve) => {
|
|
const sample = () => {
|
|
const detail = document.querySelector<HTMLElement>(
|
|
'[data-testid="scheduled-condition-detail-scroll"]'
|
|
);
|
|
if (detail) {
|
|
heights.push(detail.getBoundingClientRect().height);
|
|
}
|
|
|
|
if (performance.now() - startedAt >= 650) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
requestAnimationFrame(sample);
|
|
};
|
|
|
|
requestAnimationFrame(sample);
|
|
});
|
|
|
|
return heights;
|
|
});
|
|
|
|
expect(sampledHeights.length).toBeGreaterThan(1);
|
|
expect(Math.min(...sampledHeights)).toBeGreaterThanOrEqual(baselineHeight - 1);
|
|
await panel
|
|
.getByTestId("scheduled-condition-timeline")
|
|
.getByRole("button", { name: /SCADA 数据诊断/ })
|
|
.first()
|
|
.click();
|
|
const workflowTitle = panel.getByText("SCADA 诊断流程", { exact: true });
|
|
await expect(workflowTitle).toBeVisible();
|
|
await expect(workflowTitle.locator("..").locator("svg")).toHaveCount(0);
|
|
});
|
|
|
|
test("keeps execution spinners without breathing badges after selecting timeline tasks", async ({
|
|
page
|
|
}) => {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
|
|
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
await panel.getByRole("button", { name: "展开工况任务" }).click();
|
|
const timeline = panel.getByTestId("scheduled-condition-timeline");
|
|
const runningCondition = timeline
|
|
.locator('button[aria-pressed]')
|
|
.filter({ hasText: "SCADA 数据诊断" })
|
|
.filter({ hasText: "执行中" })
|
|
.first();
|
|
const executingWorkOrder = timeline
|
|
.locator('button[aria-pressed]')
|
|
.filter({ hasText: "系统工单:北辰分区阀门开度调整" });
|
|
|
|
await expectRunningStatusAnimations(runningCondition);
|
|
await runningCondition.click();
|
|
await expectRunningStatusAnimations(runningCondition);
|
|
await expect(
|
|
panel
|
|
.getByText("SCADA 诊断流程", { exact: true })
|
|
.locator("..")
|
|
.locator(".status-badge .status-dot")
|
|
).toHaveCount(0);
|
|
|
|
await expectRunningStatusAnimations(executingWorkOrder);
|
|
await expect(executingWorkOrder.getByText("执行中", { exact: true })).toBeVisible();
|
|
await executingWorkOrder.click();
|
|
await expectRunningStatusAnimations(executingWorkOrder);
|
|
});
|
|
|
|
test("shows completed field work as a static waiting-for-reply state", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
|
|
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
await panel.getByRole("button", { name: "展开工况任务" }).click();
|
|
const awaitingReplyWorkOrder = panel
|
|
.getByTestId("scheduled-condition-timeline")
|
|
.locator('button[aria-pressed]')
|
|
.filter({ hasText: "系统工单:泵站边界复核复令" });
|
|
|
|
await expect(awaitingReplyWorkOrder.getByText("待复令", { exact: true })).toBeVisible();
|
|
await expect(awaitingReplyWorkOrder.locator(".status-icon")).toHaveAttribute(
|
|
"data-status-tone",
|
|
"warning"
|
|
);
|
|
await expect(awaitingReplyWorkOrder.locator(".status-glyph")).toHaveAttribute(
|
|
"data-activity",
|
|
"static"
|
|
);
|
|
await expect.poll(() => getAnimationName(awaitingReplyWorkOrder.locator(".status-glyph"))).toBe(
|
|
"none"
|
|
);
|
|
|
|
await awaitingReplyWorkOrder.click();
|
|
await expect(panel.getByText("当前阶段:待复令", { exact: true })).toBeVisible();
|
|
await expect(
|
|
panel
|
|
.getByTestId("scheduled-condition-detail-scroll")
|
|
.locator('.status-badge[data-status-tone="warning"]')
|
|
.filter({ hasText: "待复令" })
|
|
.first()
|
|
).toHaveAttribute("data-status-tone", "warning");
|
|
});
|
|
|
|
async function readScrollbarPresentation(locator: Locator) {
|
|
return locator.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
const scrollbarStyle = getComputedStyle(element, "::-webkit-scrollbar");
|
|
const thumbStyle = getComputedStyle(element, "::-webkit-scrollbar-thumb");
|
|
|
|
return {
|
|
gutter: style.scrollbarGutter,
|
|
overflowY: style.overflowY,
|
|
scrollbarColor: style.scrollbarColor,
|
|
scrollbarWidth: style.scrollbarWidth,
|
|
thumbBackground: thumbStyle.backgroundColor,
|
|
thumbRadius: thumbStyle.borderRadius,
|
|
webkitWidth: scrollbarStyle.width
|
|
};
|
|
});
|
|
}
|
|
|
|
async function expectAcrylicComposition(panel: Locator) {
|
|
const composition = await panel.evaluate((element) => {
|
|
const rootStyle = getComputedStyle(element);
|
|
const blockingAncestors: string[] = [];
|
|
const filteredDescendants: string[] = [];
|
|
|
|
let ancestor = element.parentElement;
|
|
while (ancestor && ancestor.tagName !== "MAIN") {
|
|
const style = getComputedStyle(ancestor);
|
|
if (
|
|
style.transform !== "none" ||
|
|
style.filter !== "none" ||
|
|
style.opacity !== "1" ||
|
|
style.isolation !== "auto" ||
|
|
style.willChange.includes("transform")
|
|
) {
|
|
blockingAncestors.push(ancestor.className);
|
|
}
|
|
ancestor = ancestor.parentElement;
|
|
}
|
|
|
|
for (const descendant of element.querySelectorAll("*")) {
|
|
if (getComputedStyle(descendant).backdropFilter !== "none") {
|
|
filteredDescendants.push(descendant.className);
|
|
}
|
|
}
|
|
|
|
return {
|
|
backdropFilter: rootStyle.backdropFilter,
|
|
blockingAncestors,
|
|
filteredDescendants,
|
|
transform: rootStyle.transform,
|
|
willChange: rootStyle.willChange
|
|
};
|
|
});
|
|
|
|
expect(composition.backdropFilter).toContain("blur(");
|
|
expect(composition.blockingAncestors).toEqual([]);
|
|
expect(composition.filteredDescendants).toEqual([]);
|
|
expect(composition.transform).toBe("none");
|
|
expect(composition.willChange).toBe("auto");
|
|
}
|
|
|
|
async function expectRunningStatusAnimations(row: Locator) {
|
|
const timelineGlyph = row.locator(".status-icon .status-glyph");
|
|
|
|
await expect(timelineGlyph).toHaveAttribute("data-activity", "loading");
|
|
await expect(row.locator(".status-badge .status-dot")).toHaveCount(0);
|
|
await expect.poll(() => getAnimationName(timelineGlyph)).toBe("status-spin");
|
|
}
|
|
|
|
async function getAnimationName(locator: Locator) {
|
|
return locator.evaluate((element) => getComputedStyle(element).animationName);
|
|
}
|