Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
209ee8c2ea | ||
|
|
3de96f9d7b |
@@ -15,7 +15,7 @@ function subscribeCompact(listener: () => void) {
|
|||||||
media.addEventListener("change", listener);
|
media.addEventListener("change", listener);
|
||||||
return () => media.removeEventListener("change", listener);
|
return () => media.removeEventListener("change", listener);
|
||||||
}
|
}
|
||||||
const rowClass = "min-h-8 gap-2 text-xs active:bg-accent";
|
const rowClass = "min-h-8 gap-2 text-xs transition-none active:bg-accent";
|
||||||
|
|
||||||
export function PresentationHistoryMenu({ entries, selectedId, mergedEntry, mergedEntries, onSelect }: PresentationHistoryControls) {
|
export function PresentationHistoryMenu({ entries, selectedId, mergedEntry, mergedEntries, onSelect }: PresentationHistoryControls) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|||||||
@@ -82,6 +82,21 @@ describe("map layer visibility fades", () => {
|
|||||||
expect(frames.size).toBe(0);
|
expect(frames.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps opacity in range when the first frame timestamp precedes the fade start", () => {
|
||||||
|
const fixture = createMap();
|
||||||
|
now = 100;
|
||||||
|
setMapLayersVisibility(fixture.map, ["simulation-backend-analysis-label", "pipes"], false);
|
||||||
|
// RAF reports the frame start, which can precede performance.now() sampled
|
||||||
|
// later in that frame when a visibility change begins.
|
||||||
|
advance(75.8);
|
||||||
|
expect(fixture.paint.get("simulation-backend-analysis-label:text-opacity")).toBe(1);
|
||||||
|
expect(fixture.paint.get("pipes:line-opacity")).toBe(0.92);
|
||||||
|
advance(260);
|
||||||
|
expect(fixture.paint.get("simulation-backend-analysis-label:text-opacity")).toBe(0.5);
|
||||||
|
advance(420);
|
||||||
|
expect(fixture.visibility.get("simulation-backend-analysis-label")).toBe("none");
|
||||||
|
});
|
||||||
|
|
||||||
it("starts fading in after vector tiles are ready", () => {
|
it("starts fading in after vector tiles are ready", () => {
|
||||||
const fixture = createMap();
|
const fixture = createMap();
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
@@ -102,7 +117,8 @@ describe("map layer visibility fades", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function createMap() {
|
function createMap() {
|
||||||
const layers = new Map([ ["pipes", { type: "line", source: "pipes" }], ["pipes-hit", { type: "line", source: "pipes" }] ]);
|
const layers = new Map([ ["pipes", { type: "line", source: "pipes" }], ["pipes-hit", { type: "line", source: "pipes" }],
|
||||||
|
["simulation-backend-analysis-label", { type: "symbol", source: "simulation-backend-analysis" }] ]);
|
||||||
const visibility = new Map([ ["pipes", "visible"], ["pipes-hit", "visible"] ]);
|
const visibility = new Map([ ["pipes", "visible"], ["pipes-hit", "visible"] ]);
|
||||||
const paint = new Map<string, unknown>([
|
const paint = new Map<string, unknown>([
|
||||||
["pipes:line-opacity", 0.92], ["pipes:line-opacity-transition", { duration: 120 }],
|
["pipes:line-opacity", 0.92], ["pipes:line-opacity-transition", { duration: 120 }],
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ export function setMapLayersVisibility(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
startedAt ??= now;
|
startedAt ??= now;
|
||||||
const progress = Math.min(1, (now - startedAt) / LAYER_VISIBILITY_FADE_MS);
|
// RAF's frame timestamp may precede performance.now() at the fade start.
|
||||||
|
const progress = Math.max(0, Math.min(1, (now - startedAt) / LAYER_VISIBILITY_FADE_MS));
|
||||||
fade.opacity = startOpacity + (targetOpacity - startOpacity) * progress;
|
fade.opacity = startOpacity + (targetOpacity - startOpacity) * progress;
|
||||||
if (progress >= 1) finish();
|
if (progress >= 1) finish();
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -6,6 +6,40 @@ test.beforeEach(async ({ page }) => {
|
|||||||
await prepareWorkbench(page);
|
await prepareWorkbench(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const width of [1440, 1024, 375, 320]) {
|
||||||
|
test(`header hides and restores the expanded timeline at ${width}px`, async ({ page }, testInfo) => {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
const header = page.locator(".workbench-top-bar");
|
||||||
|
const timeline = page.getByRole("region", {
|
||||||
|
name: width >= 1024 ? "运行时间轴" : "移动端运行时间轴摘要", exact: true
|
||||||
|
});
|
||||||
|
const hide = header.getByRole("button", { name: "隐藏运行时间轴", exact: true });
|
||||||
|
await expect(timeline).toBeVisible();
|
||||||
|
await expect(hide).toHaveAttribute("aria-pressed", "true");
|
||||||
|
const bounds = (await hide.boundingBox())!;
|
||||||
|
expect(bounds.x).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(bounds.x + bounds.width).toBeLessThanOrEqual(width);
|
||||||
|
if (width >= 1024) await timeline.getByRole("button", { name: "展开时间轴", exact: true }).click();
|
||||||
|
else {
|
||||||
|
await timeline.getByRole("button", { name: "打开完整运行时间轴", exact: true }).click();
|
||||||
|
await expect(page.getByRole("region", { name: "运行时间轴抽屉", exact: true })).toBeVisible();
|
||||||
|
}
|
||||||
|
await hide.click();
|
||||||
|
await expect(timeline).toBeHidden();
|
||||||
|
await expect(page.getByRole("region", { name: "运行时间轴抽屉", exact: true })).toBeHidden();
|
||||||
|
const show = header.getByRole("button", { name: "显示运行时间轴", exact: true });
|
||||||
|
await expect(show).toHaveAttribute("aria-pressed", "false");
|
||||||
|
await show.click();
|
||||||
|
await expect(timeline).toBeVisible();
|
||||||
|
await expect(hide).toHaveAttribute("aria-pressed", "true");
|
||||||
|
if (width >= 1024) await expect(timeline.getByRole("button", { name: "收起时间轴", exact: true })).toBeVisible();
|
||||||
|
await testInfo.attach(`header-timeline-${width}`, {
|
||||||
|
body: await header.screenshot(), contentType: "image/png"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test("desktop controls share focus, hover, press and open states", async ({ page }) => {
|
test("desktop controls share focus, hover, press and open states", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 1440, height: 900 });
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
import { expect, test, type Locator, type Page, type TestInfo } from "@playwright/test";
|
||||||
import { investigationFixture, mockInvestigationPresentation, selectInvestigation, showInvestigationMap } from "./support/mock-investigation-presentation";
|
import { investigationFixture, mockInvestigationPresentation, selectInvestigation, showInvestigationMap } from "./support/mock-investigation-presentation";
|
||||||
|
import { readThemeColor } from "./support/theme-colors";
|
||||||
|
|
||||||
const chart = (id: string, title: string) => ({ action: "open_chart", chart_id: id, evidence_refs: ["synthetic:reading"],
|
const chart = (id: string, title: string) => ({ action: "open_chart", chart_id: id, evidence_refs: ["synthetic:reading"],
|
||||||
chart: { title, chart_type: "line", x_data: ["08:00", "08:15"], series: [{ name: "压力(m)", data: [30, 28] }] } });
|
chart: { title, chart_type: "line", x_data: ["08:00", "08:15"], series: [{ name: "压力(m)", data: [30, 28] }] } });
|
||||||
|
|
||||||
|
async function sampleFastHoverBackground(items: Locator[]) {
|
||||||
|
const samples: Array<{ actual: string; expected: string }> = [];
|
||||||
|
for (let repeat = 0; repeat < 3; repeat++) {
|
||||||
|
for (const item of items) {
|
||||||
|
const expected = await readThemeColor(item, "--action-selection-soft");
|
||||||
|
await item.hover();
|
||||||
|
const actual = await item.evaluate((node) => new Promise<string>((resolve) => {
|
||||||
|
requestAnimationFrame(() => resolve(getComputedStyle(node).backgroundColor));
|
||||||
|
}));
|
||||||
|
samples.push({ actual, expected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
async function captureMenus(page: Page, testInfo: TestInfo, name: string) {
|
async function captureMenus(page: Page, testInfo: TestInfo, name: string) {
|
||||||
for (const menu of await page.getByRole("menu").all()) {
|
for (const menu of await page.getByRole("menu").all()) {
|
||||||
await menu.evaluate((node) => Promise.all(node.getAnimations().map((animation) => animation.finished.catch(() => undefined))));
|
await menu.evaluate((node) => Promise.all(node.getAnimations().map((animation) => animation.finished.catch(() => undefined))));
|
||||||
@@ -56,8 +72,18 @@ test("hover keeps an expanded category stable across its title, arrow and origin
|
|||||||
const arrow = root.getByRole("menuitem", { name: "展开图表步骤", exact: true });
|
const arrow = root.getByRole("menuitem", { name: "展开图表步骤", exact: true });
|
||||||
const title = root.getByRole("menuitem", { name: "图表", exact: true });
|
const title = root.getByRole("menuitem", { name: "图表", exact: true });
|
||||||
const steps = page.getByRole("menu", { name: "图表展示步骤", exact: true });
|
const steps = page.getByRole("menu", { name: "图表展示步骤", exact: true });
|
||||||
|
const firstLevel = await sampleFastHoverBackground([title, root.getByRole("menuitem", { name: "覆盖层", exact: true })]);
|
||||||
await arrow.hover();
|
await arrow.hover();
|
||||||
await expect(steps).toBeVisible();
|
await expect(steps).toBeVisible();
|
||||||
|
const secondLevel = await sampleFastHoverBackground([
|
||||||
|
steps.getByRole("menuitem", { name: "1. 测点 P1 压力趋势", exact: true }),
|
||||||
|
steps.getByRole("menuitem", { name: "2. 测点 P2 压力趋势", exact: true })
|
||||||
|
]);
|
||||||
|
const backgrounds = [...firstLevel, ...secondLevel];
|
||||||
|
await testInfo.attach("fast-hover-backgrounds", { body: JSON.stringify({ firstLevel, secondLevel }), contentType: "application/json" });
|
||||||
|
expect(backgrounds).toHaveLength(12);
|
||||||
|
expect(backgrounds.every((sample) => sample.expected !== "rgba(0, 0, 0, 0)")).toBe(true);
|
||||||
|
expect(backgrounds.map((sample) => sample.actual)).toEqual(backgrounds.map((sample) => sample.expected));
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const changes: string[] = [];
|
const changes: string[] = [];
|
||||||
const observer = new MutationObserver((records) => {
|
const observer = new MutationObserver((records) => {
|
||||||
|
|||||||
@@ -48,6 +48,27 @@ async function openLayerFixture(page: Page, vectorPipes = false) {
|
|||||||
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("analysis label fade accepts a frame timestamp earlier than the visibility change", async ({ page }) => {
|
||||||
|
await openLayerFixture(page);
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const modulePath = "/src/features/workbench/map/layer-visibility.ts";
|
||||||
|
const { setMapLayersVisibility } = await import(modulePath);
|
||||||
|
const map = window.__waterNetworkMap!;
|
||||||
|
const errors: string[] = [];
|
||||||
|
map.on("error", (event) => {
|
||||||
|
if (event.error.message.includes("simulation-backend-analysis-label")) errors.push(event.error.message);
|
||||||
|
});
|
||||||
|
Object.assign(window, { analysisFadeErrors: errors });
|
||||||
|
const nativeFrame = window.requestAnimationFrame.bind(window);
|
||||||
|
window.requestAnimationFrame = (callback) => nativeFrame((time) => callback(time - 100));
|
||||||
|
try { setMapLayersVisibility(map, ["simulation-backend-analysis-label"], false); }
|
||||||
|
finally { window.requestAnimationFrame = nativeFrame; }
|
||||||
|
});
|
||||||
|
await expect.poll(() => page.evaluate(() => window.__waterNetworkMap!
|
||||||
|
.getLayoutProperty("simulation-backend-analysis-label", "visibility"))).toBe("none");
|
||||||
|
expect(await page.evaluate(() => (window as unknown as { analysisFadeErrors: string[] }).analysisFadeErrors)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
for (const [label, layerId, property] of [
|
for (const [label, layerId, property] of [
|
||||||
["管线", "pipes-flow", "line-opacity"],
|
["管线", "pipes-flow", "line-opacity"],
|
||||||
["水库", "reservoirs-symbol", "icon-opacity"],
|
["水库", "reservoirs-symbol", "icon-opacity"],
|
||||||
|
|||||||
Reference in New Issue
Block a user