feat: refine workbench visuals and map controls
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.
This commit is contained in:
@@ -1,29 +1,33 @@
|
||||
import { expect, test, type Locator } from "playwright/test";
|
||||
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 page.waitForTimeout(250);
|
||||
|
||||
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 page.waitForTimeout(250);
|
||||
|
||||
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)
|
||||
);
|
||||
@@ -33,9 +37,32 @@ test("scheduled conditions keep acrylic blur outside transformed ancestors", asy
|
||||
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 page.waitForTimeout(250);
|
||||
await expect(panel).toHaveCSS("width", "432px");
|
||||
const restoredTimelineBox = await timeline.boundingBox();
|
||||
expect(restoredTimelineBox).not.toBeNull();
|
||||
@@ -82,6 +109,94 @@ test("narrow desktop keeps Agent and expanded conditions from overlapping", asyn
|
||||
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" });
|
||||
|
||||
@@ -93,13 +208,47 @@ test("shows completed field work as a static waiting-for-reply state", async ({
|
||||
.filter({ hasText: "系统工单:泵站边界复核复令" });
|
||||
|
||||
await expect(awaitingReplyWorkOrder.getByText("待复令", { exact: true })).toBeVisible();
|
||||
await expect(awaitingReplyWorkOrder.locator(".animate-spin")).toHaveCount(0);
|
||||
await expect(awaitingReplyWorkOrder.locator(".bg-orange-50")).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);
|
||||
@@ -142,3 +291,15 @@ async function expectAcrylicComposition(panel: Locator) {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user