310 lines
12 KiB
TypeScript
310 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" });
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
|
|
|
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" });
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
|
|
|
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" });
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
|
|
|
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" });
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
|
|
|
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);
|
|
}
|