fix(ui): refine acrylic controls and timeline interactions

Restore full acrylic materials to stacked task cards and floating controls.
Extend the map behind the navigation header while preserving document and
control insets. Align timeline and scale readouts, refresh dense-marker
hover content, and highlight model and approval menu options.

Keep narrow timeline headers clear of their legend and actions. Update
coupled geometry checks and retain city zoom-12 and menu regression coverage.

Validation: scoped git diff --check and read-only UI geometry inspection.
Test suites were not rerun at the user's request.
This commit is contained in:
2026-10-09 18:11:33 +08:00
parent 617ba4d87c
commit 85a9a3ab45
16 changed files with 246 additions and 86 deletions
+2 -1
View File
@@ -94,7 +94,8 @@ test("desktop Agent docks beside the workspace with a solid reading surface", as
const headerBox = await agentHeader.boundingBox();
const workspaceBox = await workspace.boundingBox();
expect(headerBox!.height).toBe(topBarBox!.height);
expect(headerBox!.y).toBe(workspaceBox!.y);
expect(headerBox!.y).toBe(topBarBox!.y + topBarBox!.height);
expect(workspaceBox!.y).toBe(topBarBox!.y);
await expect(topBar).toHaveCSS("box-shadow", "none");
await expect(agentHeader).toHaveCSS("box-shadow", "none");
await expect(agentHeader.locator(":scope > div").first()).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
+95
View File
@@ -0,0 +1,95 @@
import { expect, test, type Locator } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
import { expectThemeColor } from "./support/theme-colors";
test.beforeEach(async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await mockWorkbenchContext(page);
await mockAgentApi(page);
await page.route("**/api/v1/agent/chat/models", (route) => route.fulfill({
headers: { "Access-Control-Allow-Origin": "*" },
json: {
default_model: "test/fast",
models: [
{ id: "test/fast", label: "快速", description: "快速回答和日常任务处理", icon: "bolt" },
{ id: "test/expert", label: "专家", description: "深入分析和复杂任务处理", icon: "sparkle" }
]
}
}));
});
for (const width of [1440, 375]) {
test(`model options show hover and keyboard highlight without changing selection at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
// Radix hides the trigger from the accessibility tree while its modal menu is open.
const trigger = page.locator('button[aria-label="选择 Agent 模型"]');
await expect(trigger).toHaveAccessibleDescription("当前模型:快速");
await trigger.click();
await waitForMenuAnimations(page.getByRole("menu"));
const fast = page.getByRole("menuitem", { name: /快速回答和日常任务处理/ });
const expert = page.getByRole("menuitem", { name: /深入分析和复杂任务处理/ });
const box = await expert.boundingBox();
for (const item of [fast, expert]) {
await item.hover();
await expect(item).toHaveAttribute("data-highlighted", "");
await expectThemeColor(item, "background-color", "--action-selection-soft-hover");
await expectThemeColor(item, "border-color", "--action-border-hover");
await expect(trigger).toHaveAttribute("aria-description", "当前模型:快速");
}
expect(await expert.boundingBox()).toEqual(box);
await page.getByRole("menu").screenshot({ path: `test-results/agent-select-menu/model-${width}.png` });
await page.keyboard.press("ArrowUp");
await expect(fast).toBeFocused();
await expectThemeColor(fast, "background-color", "--action-selection-soft-hover");
await page.keyboard.press("ArrowDown");
await expect(expert).toBeFocused();
await page.keyboard.press("Enter");
await expect(page.getByRole("menu")).toHaveCount(0);
await expect(trigger).toHaveAccessibleDescription("当前模型:专家");
await expect(trigger).toBeFocused();
});
test(`approval options highlight both selected and unselected modes at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
const trigger = page.locator('button[aria-label="权限批准模式"]');
await trigger.click();
await waitForMenuAnimations(page.getByRole("menu"));
const request = page.getByRole("menuitem", { name: /工具权限逐次确认/ });
const always = page.getByRole("menuitem", { name: /自动允许本轮权限请求/ });
for (const item of [request, always]) {
await item.hover();
await expectThemeColor(item, "background-color", "--action-selection-soft-hover");
await expectThemeColor(item, "border-color", "--action-border-hover");
await expect(trigger).toContainText("请求批准");
}
await page.getByRole("menu").screenshot({ path: `test-results/agent-select-menu/approval-${width}.png` });
await always.click();
await expect(page.getByRole("menu")).toHaveCount(0);
await expect(trigger).toBeFocused();
await expect(trigger).toContainText("始终允许");
await trigger.click();
await waitForMenuAnimations(page.getByRole("menu"));
await always.hover();
await expectThemeColor(always, "background-color", "--action-selection-soft-hover");
await page.keyboard.press("ArrowUp");
await expect(request).toBeFocused();
await expectThemeColor(request, "background-color", "--action-selection-soft-hover");
await page.keyboard.press("Enter");
await expect(trigger).toContainText("请求批准");
await expect(trigger).toBeFocused();
});
}
async function waitForMenuAnimations(menu: Locator) {
await expect(menu).toBeVisible();
await menu.evaluate((element) => Promise.all(
element.getAnimations({ subtree: true })
.filter((animation) => animation.playState === "running")
.map((animation) => animation.finished.catch(() => undefined))
));
}
+44
View File
@@ -65,6 +65,50 @@ async function waitForMapIdle(page: Page) {
);
}
test("city buildings render only from zoom 12 in 3D", async ({ page }) => {
test.skip(
process.env.TJWATER_CITY_SCENE_TEST !== "1",
"Requires the generated local city-model scene"
);
test.setTimeout(60_000);
await mockWorkbenchContext(page, undefined, undefined, {}, [], {
tiandituToken: "city-scene-tianditu-token"
});
await page.route("https://t0.tianditu.gov.cn/**", (route) =>
route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE })
);
await page.goto("/", { waitUntil: "domcontentloaded" });
const toggle = page
.getByRole("group", { name: "地图缩放", exact: true })
.locator("button[aria-pressed]");
await expect(toggle).toBeEnabled({ timeout: 15_000 });
await toggle.click();
await page.evaluate(() =>
window.__waterNetworkMap!.jumpTo({ center: [121.464, 30.916], zoom: 12, pitch: 55, bearing: 0 })
);
const renderedBuildings = () => page.evaluate(() => {
const map = window.__waterNetworkMap!;
return map.getLayer("fengxian-city-buildings")
? map.queryRenderedFeatures({ layers: ["fengxian-city-buildings"] }).length
: 0;
});
await expect.poll(renderedBuildings, { timeout: 30_000 }).toBeGreaterThan(0);
await page.screenshot({ path: "test-results/city-model/zoom-12.png" });
for (const zoom of [11.99, 11]) {
await page.evaluate((value) => window.__waterNetworkMap!.jumpTo({ zoom: value }), zoom);
await expect.poll(renderedBuildings).toBe(0);
}
await page.screenshot({ path: "test-results/city-model/zoom-11.png" });
for (const zoom of [12, 15, 16]) {
await page.evaluate((value) => window.__waterNetworkMap!.jumpTo({ zoom: value }), zoom);
await expect.poll(renderedBuildings, { timeout: 15_000 }).toBeGreaterThan(0);
}
await toggle.click();
await expect.poll(() => page.evaluate(() => window.__waterNetworkMap!.getPitch())).toBe(0);
await expect.poll(renderedBuildings).toBe(0);
});
// Uses generated local building data, while unrelated backend services are isolated.
test("3D adds opaque buildings without enabling duplicate GIS layers", async ({ page }) => {
test.skip(
+2 -1
View File
@@ -301,7 +301,8 @@ async function expectDesktopMainFrameGeometry(page: Page) {
expect(agentBox!.x).toBe(0);
expect(agentBox!.y).toBe(56);
expect(workspaceBox!.x).toBe(agentBox!.x + agentBox!.width);
expect(workspaceBox!.y).toBe(agentBox!.y);
expect(workspaceBox!.y).toBe(0);
expect(workspaceBox!.height).toBe(agentBox!.height + agentBox!.y);
expect(mapBox).toEqual(workspaceBox);
}