refactor: simplify workbench layout and browser tests
This commit is contained in:
@@ -2,12 +2,16 @@ import { defineConfig, devices } from "@playwright/test";
|
|||||||
|
|
||||||
const testPort = process.env.PLAYWRIGHT_PORT || "5191";
|
const testPort = process.env.PLAYWRIGHT_PORT || "5191";
|
||||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://127.0.0.1:${testPort}`;
|
const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://127.0.0.1:${testPort}`;
|
||||||
|
const runningInCi = Boolean(process.env.CI);
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: ".",
|
testDir: ".",
|
||||||
testMatch: ["src/**/*.e2e.ts", "tests/browser/**/*.e2e.ts"],
|
testMatch: ["src/**/*.e2e.ts", "tests/browser/**/*.e2e.ts"],
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
reporter: "html",
|
forbidOnly: runningInCi,
|
||||||
|
retries: runningInCi ? 1 : 0,
|
||||||
|
workers: runningInCi ? 2 : undefined,
|
||||||
|
reporter: runningInCi ? [["line"], ["html", { open: "never" }]] : "html",
|
||||||
use: {
|
use: {
|
||||||
baseURL,
|
baseURL,
|
||||||
locale: "zh-CN",
|
locale: "zh-CN",
|
||||||
|
|||||||
@@ -168,9 +168,6 @@ export function useWorkbenchMap({
|
|||||||
|
|
||||||
const resizeMap = () => map.resize();
|
const resizeMap = () => map.resize();
|
||||||
window.addEventListener("resize", resizeMap);
|
window.addEventListener("resize", resizeMap);
|
||||||
window.setTimeout(resizeMap, 0);
|
|
||||||
window.setTimeout(resizeMap, 300);
|
|
||||||
window.setTimeout(resizeMap, 1000);
|
|
||||||
|
|
||||||
map.on("sourcedata", (event) => {
|
map.on("sourcedata", (event) => {
|
||||||
updateStatusFromSourceEvent(event, "online", setSourceStatuses);
|
updateStatusFromSourceEvent(event, "online", setSourceStatuses);
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { act, renderHook } from "@testing-library/react";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { useWorkbenchResponsiveLayout } from "./use-workbench-responsive-layout";
|
||||||
|
|
||||||
|
function mockViewport(width: number, largeScreen: boolean) {
|
||||||
|
Object.defineProperty(window, "innerWidth", {
|
||||||
|
configurable: true,
|
||||||
|
value: width
|
||||||
|
});
|
||||||
|
vi.stubGlobal(
|
||||||
|
"matchMedia",
|
||||||
|
vi.fn().mockImplementation((query: string) => ({
|
||||||
|
matches: largeScreen,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
addListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn()
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("workbench responsive layout", () => {
|
||||||
|
it("coordinates the expanded condition feed with the desktop Agent panel", () => {
|
||||||
|
mockViewport(1366, true);
|
||||||
|
const collapseAgentPanel = vi.fn();
|
||||||
|
const expandAgentPanel = vi.fn();
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useWorkbenchResponsiveLayout({
|
||||||
|
activeToolOpen: false,
|
||||||
|
agentPanelOpen: true,
|
||||||
|
devPanelOpen: false,
|
||||||
|
collapseAgentPanel,
|
||||||
|
expandAgentPanel,
|
||||||
|
onClearActiveTool: vi.fn()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.isLargeScreen).toBe(true);
|
||||||
|
expect(result.current.shouldShowConditionFeed).toBe(true);
|
||||||
|
|
||||||
|
act(() => result.current.handleConditionExpandedChange(true));
|
||||||
|
expect(result.current.conditionFeedExpanded).toBe(true);
|
||||||
|
expect(collapseAgentPanel).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
act(() => result.current.handleConditionExpandedChange(false));
|
||||||
|
expect(result.current.conditionFeedExpanded).toBe(false);
|
||||||
|
expect(expandAgentPanel).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles the condition sheet without leaking desktop panel state on mobile", () => {
|
||||||
|
mockViewport(390, false);
|
||||||
|
const onClearActiveTool = vi.fn();
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useWorkbenchResponsiveLayout({
|
||||||
|
activeToolOpen: true,
|
||||||
|
agentPanelOpen: true,
|
||||||
|
devPanelOpen: false,
|
||||||
|
collapseAgentPanel: vi.fn(),
|
||||||
|
expandAgentPanel: vi.fn(),
|
||||||
|
onClearActiveTool
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => result.current.toggleConditionFeedForViewport());
|
||||||
|
expect(result.current.mobileSheet).toBe("condition");
|
||||||
|
expect(result.current.mobileSheetSnap).toBe("half");
|
||||||
|
expect(onClearActiveTool).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
act(() => result.current.toggleConditionFeedForViewport());
|
||||||
|
expect(result.current.mobileSheet).toBeNull();
|
||||||
|
|
||||||
|
act(() => result.current.openAgentPanelForViewport());
|
||||||
|
expect(result.current.mobileSheet).toBe("agent");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { MobileWorkbenchSheetSnap } from "../components/mobile-workbench-sheet";
|
||||||
|
import { WORKBENCH_LAYOUT } from "../layout/workbench-layout";
|
||||||
|
|
||||||
|
const CONDITION_FEED_EXIT_MS = 170;
|
||||||
|
const CONDITION_FEED_EXPANDED_MIN_WIDTH = 1440;
|
||||||
|
const CONDITION_FEED_DEFAULT_MIN_WIDTH = 1280;
|
||||||
|
const LARGE_SCREEN_QUERY = "(min-width: 1024px)";
|
||||||
|
|
||||||
|
type WorkbenchMobileSheet = "agent" | "condition" | null;
|
||||||
|
|
||||||
|
type UseWorkbenchResponsiveLayoutOptions = {
|
||||||
|
activeToolOpen: boolean;
|
||||||
|
agentPanelOpen: boolean;
|
||||||
|
devPanelOpen: boolean;
|
||||||
|
collapseAgentPanel: () => void;
|
||||||
|
expandAgentPanel: () => void;
|
||||||
|
onClearActiveTool: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWorkbenchResponsiveLayout({
|
||||||
|
activeToolOpen,
|
||||||
|
agentPanelOpen,
|
||||||
|
devPanelOpen,
|
||||||
|
collapseAgentPanel,
|
||||||
|
expandAgentPanel,
|
||||||
|
onClearActiveTool
|
||||||
|
}: UseWorkbenchResponsiveLayoutOptions) {
|
||||||
|
const [isLargeScreen, setIsLargeScreen] = useState(false);
|
||||||
|
const [viewportWidth, setViewportWidth] = useState<number>(WORKBENCH_LAYOUT.desktopMinWidth);
|
||||||
|
const [agentPanelWidth, setAgentPanelWidth] = useState<number>(
|
||||||
|
WORKBENCH_LAYOUT.desktop.agentWidth
|
||||||
|
);
|
||||||
|
const [conditionFeedVisible, setConditionFeedVisible] = useState(false);
|
||||||
|
const [conditionFeedMounted, setConditionFeedMounted] = useState(true);
|
||||||
|
const [conditionFeedExpanded, setConditionFeedExpanded] = useState(false);
|
||||||
|
const [mobileSheet, setMobileSheet] = useState<WorkbenchMobileSheet>(null);
|
||||||
|
const [mobileSheetSnap, setMobileSheetSnap] = useState<MobileWorkbenchSheetSnap>("half");
|
||||||
|
const [agentCollapsedForCondition, setAgentCollapsedForCondition] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia(LARGE_SCREEN_QUERY);
|
||||||
|
const handleViewportChange = () => {
|
||||||
|
setIsLargeScreen(mediaQuery.matches);
|
||||||
|
setViewportWidth(window.innerWidth);
|
||||||
|
if (mediaQuery.matches) {
|
||||||
|
setMobileSheet(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleViewportChange();
|
||||||
|
setConditionFeedVisible(window.innerWidth >= CONDITION_FEED_DEFAULT_MIN_WIDTH);
|
||||||
|
mediaQuery.addEventListener("change", handleViewportChange);
|
||||||
|
window.addEventListener("resize", handleViewportChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener("change", handleViewportChange);
|
||||||
|
window.removeEventListener("resize", handleViewportChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const shouldShowConditionFeed = conditionFeedVisible && !activeToolOpen;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldShowConditionFeed) {
|
||||||
|
setConditionFeedMounted(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
setConditionFeedMounted(false);
|
||||||
|
}, CONDITION_FEED_EXIT_MS);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
|
}, [shouldShowConditionFeed]);
|
||||||
|
|
||||||
|
function closeMobileSheet() {
|
||||||
|
setMobileSheet(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAgentPanelForViewport() {
|
||||||
|
if (isLargeScreen) {
|
||||||
|
if (conditionFeedExpanded && viewportWidth < CONDITION_FEED_EXPANDED_MIN_WIDTH) {
|
||||||
|
setConditionFeedExpanded(false);
|
||||||
|
setAgentCollapsedForCondition(false);
|
||||||
|
}
|
||||||
|
expandAgentPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMobileSheet("agent");
|
||||||
|
setMobileSheetSnap("half");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConditionExpandedChange(nextExpanded: boolean) {
|
||||||
|
if (nextExpanded && isLargeScreen && viewportWidth < CONDITION_FEED_EXPANDED_MIN_WIDTH) {
|
||||||
|
setAgentCollapsedForCondition(agentPanelOpen);
|
||||||
|
collapseAgentPanel();
|
||||||
|
} else if (!nextExpanded && agentCollapsedForCondition) {
|
||||||
|
setAgentCollapsedForCondition(false);
|
||||||
|
expandAgentPanel();
|
||||||
|
}
|
||||||
|
setConditionFeedExpanded(nextExpanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLargeScreen || shouldShowConditionFeed || !conditionFeedExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConditionFeedExpanded(false);
|
||||||
|
if (agentCollapsedForCondition) {
|
||||||
|
setAgentCollapsedForCondition(false);
|
||||||
|
expandAgentPanel();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
agentCollapsedForCondition,
|
||||||
|
conditionFeedExpanded,
|
||||||
|
expandAgentPanel,
|
||||||
|
isLargeScreen,
|
||||||
|
shouldShowConditionFeed
|
||||||
|
]);
|
||||||
|
|
||||||
|
function toggleConditionFeedForViewport() {
|
||||||
|
onClearActiveTool();
|
||||||
|
if (isLargeScreen) {
|
||||||
|
setConditionFeedVisible((current) => !current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobileSheet === "condition") {
|
||||||
|
closeMobileSheet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConditionFeedExpanded(false);
|
||||||
|
setMobileSheetSnap("half");
|
||||||
|
setMobileSheet("condition");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConditionFeedForViewport() {
|
||||||
|
setConditionFeedVisible(true);
|
||||||
|
onClearActiveTool();
|
||||||
|
if (!isLargeScreen) {
|
||||||
|
setMobileSheet("condition");
|
||||||
|
setMobileSheetSnap("half");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftPanelOpen = isLargeScreen && agentPanelOpen;
|
||||||
|
const rightPanelOpen = isLargeScreen && (devPanelOpen || shouldShowConditionFeed);
|
||||||
|
const rightPanelExpanded =
|
||||||
|
isLargeScreen && shouldShowConditionFeed && conditionFeedExpanded && !devPanelOpen;
|
||||||
|
|
||||||
|
return {
|
||||||
|
agentPanelWidth,
|
||||||
|
closeMobileSheet,
|
||||||
|
conditionFeedExpanded,
|
||||||
|
conditionFeedMounted,
|
||||||
|
handleConditionExpandedChange,
|
||||||
|
isLargeScreen,
|
||||||
|
leftPanelOpen,
|
||||||
|
mobileSheet,
|
||||||
|
mobileSheetSnap,
|
||||||
|
openAgentPanelForViewport,
|
||||||
|
openConditionFeedForViewport,
|
||||||
|
rightPanelExpanded,
|
||||||
|
rightPanelOpen,
|
||||||
|
setAgentPanelWidth,
|
||||||
|
setMobileSheetSnap,
|
||||||
|
shouldShowConditionFeed,
|
||||||
|
toggleConditionFeedForViewport,
|
||||||
|
viewportWidth
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -31,10 +31,7 @@ import {
|
|||||||
import { env } from "@/shared/config/env";
|
import { env } from "@/shared/config/env";
|
||||||
import { AgentTaskTicker } from "./components/agent-task-ticker";
|
import { AgentTaskTicker } from "./components/agent-task-ticker";
|
||||||
import { MapDevPanel } from "./components/map-dev-panel";
|
import { MapDevPanel } from "./components/map-dev-panel";
|
||||||
import {
|
import { MobileWorkbenchSheet } from "./components/mobile-workbench-sheet";
|
||||||
MobileWorkbenchSheet,
|
|
||||||
type MobileWorkbenchSheetSnap
|
|
||||||
} from "./components/mobile-workbench-sheet";
|
|
||||||
import { FeaturePopover } from "./components/feature-popover";
|
import { FeaturePopover } from "./components/feature-popover";
|
||||||
import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
|
import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
|
||||||
import { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
|
import { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
|
||||||
@@ -50,6 +47,7 @@ import { useWorkbenchAgent } from "./hooks/use-workbench-agent";
|
|||||||
import { useWorkbenchDrawing, type WorkbenchDrawMode } from "./hooks/use-workbench-drawing";
|
import { useWorkbenchDrawing, type WorkbenchDrawMode } from "./hooks/use-workbench-drawing";
|
||||||
import { useWorkbenchMap } from "./hooks/use-workbench-map";
|
import { useWorkbenchMap } from "./hooks/use-workbench-map";
|
||||||
import { useWorkbenchMapController } from "./hooks/use-workbench-map-controller";
|
import { useWorkbenchMapController } from "./hooks/use-workbench-map-controller";
|
||||||
|
import { useWorkbenchResponsiveLayout } from "./hooks/use-workbench-responsive-layout";
|
||||||
import { useWorkbenchRuntimeData } from "./hooks/use-workbench-runtime-data";
|
import { useWorkbenchRuntimeData } from "./hooks/use-workbench-runtime-data";
|
||||||
import { toMapFeatureReference } from "./hooks/use-map-interactions";
|
import { toMapFeatureReference } from "./hooks/use-map-interactions";
|
||||||
import {
|
import {
|
||||||
@@ -69,7 +67,6 @@ import {
|
|||||||
BASE_LAYER_OPTIONS,
|
BASE_LAYER_OPTIONS,
|
||||||
INITIAL_LAYER_VISIBILITY,
|
INITIAL_LAYER_VISIBILITY,
|
||||||
MAP_LEGEND_ITEMS,
|
MAP_LEGEND_ITEMS,
|
||||||
WORKBENCH_LAYER_GROUPS,
|
|
||||||
applyBaseLayerVisibility,
|
applyBaseLayerVisibility,
|
||||||
createLayerControlItems,
|
createLayerControlItems,
|
||||||
getWorkbenchLayerIds
|
getWorkbenchLayerIds
|
||||||
@@ -96,18 +93,7 @@ export function MapWorkbenchPage() {
|
|||||||
const [detailFeature, setDetailFeature] = useState<DetailFeature | null>(null);
|
const [detailFeature, setDetailFeature] = useState<DetailFeature | null>(null);
|
||||||
const [devPanelOpen, setDevPanelOpen] = useState(false);
|
const [devPanelOpen, setDevPanelOpen] = useState(false);
|
||||||
const [impactVisible, setImpactVisible] = useState(false);
|
const [impactVisible, setImpactVisible] = useState(false);
|
||||||
const [isLargeScreen, setIsLargeScreen] = useState(false);
|
|
||||||
const [viewportWidth, setViewportWidth] = useState<number>(WORKBENCH_LAYOUT.desktopMinWidth);
|
|
||||||
const [agentPanelWidth, setAgentPanelWidth] = useState<number>(
|
|
||||||
WORKBENCH_LAYOUT.desktop.agentWidth
|
|
||||||
);
|
|
||||||
const [activeToolId, setActiveToolId] = useState<ToolbarToolId | null>(null);
|
const [activeToolId, setActiveToolId] = useState<ToolbarToolId | null>(null);
|
||||||
const [conditionFeedVisible, setConditionFeedVisible] = useState(false);
|
|
||||||
const [conditionFeedMounted, setConditionFeedMounted] = useState(true);
|
|
||||||
const [conditionFeedExpanded, setConditionFeedExpanded] = useState(false);
|
|
||||||
const [mobileSheet, setMobileSheet] = useState<"agent" | "condition" | null>(null);
|
|
||||||
const [mobileSheetSnap, setMobileSheetSnap] = useState<MobileWorkbenchSheetSnap>("half");
|
|
||||||
const [agentCollapsedForCondition, setAgentCollapsedForCondition] = useState(false);
|
|
||||||
const prefersReducedMotion = useReducedMotion();
|
const prefersReducedMotion = useReducedMotion();
|
||||||
const [taskTickerVisible, setTaskTickerVisible] = useState(true);
|
const [taskTickerVisible, setTaskTickerVisible] = useState(true);
|
||||||
const [selectedConditionId, setSelectedConditionId] = useState<string | null>(null);
|
const [selectedConditionId, setSelectedConditionId] = useState<string | null>(null);
|
||||||
@@ -141,114 +127,40 @@ export function MapWorkbenchPage() {
|
|||||||
setDetailFeature(feature);
|
setDetailFeature(feature);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mediaQuery = window.matchMedia("(min-width: 1024px)");
|
|
||||||
const handleChange = () => {
|
|
||||||
setIsLargeScreen(mediaQuery.matches);
|
|
||||||
setViewportWidth(window.innerWidth);
|
|
||||||
if (mediaQuery.matches) {
|
|
||||||
setMobileSheet(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChange();
|
|
||||||
setConditionFeedVisible(window.innerWidth >= 1280);
|
|
||||||
mediaQuery.addEventListener("change", handleChange);
|
|
||||||
window.addEventListener("resize", handleChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mediaQuery.removeEventListener("change", handleChange);
|
|
||||||
window.removeEventListener("resize", handleChange);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const shouldShowConditionFeed = conditionFeedVisible && !activeToolId;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (shouldShowConditionFeed) {
|
|
||||||
setConditionFeedMounted(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeoutId = window.setTimeout(() => {
|
|
||||||
setConditionFeedMounted(false);
|
|
||||||
}, 170);
|
|
||||||
|
|
||||||
return () => window.clearTimeout(timeoutId);
|
|
||||||
}, [shouldShowConditionFeed]);
|
|
||||||
|
|
||||||
const agent = useWorkbenchAgent({
|
const agent = useWorkbenchAgent({
|
||||||
onUiEnvelope: handleAgentUiEnvelope,
|
onUiEnvelope: handleAgentUiEnvelope,
|
||||||
onFrontendAction: handleFrontendAction
|
onFrontendAction: handleFrontendAction
|
||||||
});
|
});
|
||||||
function openAgentPanelForViewport() {
|
const clearActiveTool = useCallback(() => {
|
||||||
if (isLargeScreen) {
|
|
||||||
if (conditionFeedExpanded && viewportWidth < 1440) {
|
|
||||||
setConditionFeedExpanded(false);
|
|
||||||
setAgentCollapsedForCondition(false);
|
|
||||||
}
|
|
||||||
agent.expandPanel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMobileSheet("agent");
|
|
||||||
setMobileSheetSnap("half");
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMobileSheet = useCallback(() => {
|
|
||||||
setMobileSheet(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function handleConditionExpandedChange(nextExpanded: boolean) {
|
|
||||||
if (nextExpanded && isLargeScreen && viewportWidth < 1440) {
|
|
||||||
setAgentCollapsedForCondition(agent.panelOpen);
|
|
||||||
agent.collapsePanel();
|
|
||||||
} else if (!nextExpanded && agentCollapsedForCondition) {
|
|
||||||
setAgentCollapsedForCondition(false);
|
|
||||||
agent.expandPanel();
|
|
||||||
}
|
|
||||||
setConditionFeedExpanded(nextExpanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLargeScreen || shouldShowConditionFeed || !conditionFeedExpanded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setConditionFeedExpanded(false);
|
|
||||||
if (agentCollapsedForCondition) {
|
|
||||||
setAgentCollapsedForCondition(false);
|
|
||||||
agent.expandPanel();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
agent,
|
|
||||||
agentCollapsedForCondition,
|
|
||||||
conditionFeedExpanded,
|
|
||||||
isLargeScreen,
|
|
||||||
shouldShowConditionFeed
|
|
||||||
]);
|
|
||||||
|
|
||||||
function toggleConditionFeedForViewport() {
|
|
||||||
setActiveToolId(null);
|
setActiveToolId(null);
|
||||||
if (isLargeScreen) {
|
}, []);
|
||||||
setConditionFeedVisible((current) => !current);
|
const {
|
||||||
return;
|
agentPanelWidth,
|
||||||
}
|
closeMobileSheet,
|
||||||
|
conditionFeedExpanded,
|
||||||
if (mobileSheet === "condition") {
|
conditionFeedMounted,
|
||||||
closeMobileSheet();
|
handleConditionExpandedChange,
|
||||||
return;
|
isLargeScreen,
|
||||||
}
|
leftPanelOpen,
|
||||||
|
mobileSheet,
|
||||||
setConditionFeedExpanded(false);
|
mobileSheetSnap,
|
||||||
setMobileSheetSnap("half");
|
openAgentPanelForViewport,
|
||||||
setMobileSheet("condition");
|
openConditionFeedForViewport,
|
||||||
}
|
rightPanelExpanded,
|
||||||
|
rightPanelOpen,
|
||||||
const leftPanelOpen = isLargeScreen && agent.panelOpen;
|
setAgentPanelWidth,
|
||||||
const rightPanelOpen = isLargeScreen && (devPanelOpen || shouldShowConditionFeed);
|
setMobileSheetSnap,
|
||||||
const rightPanelExpanded =
|
shouldShowConditionFeed,
|
||||||
isLargeScreen && shouldShowConditionFeed && conditionFeedExpanded && !devPanelOpen;
|
toggleConditionFeedForViewport,
|
||||||
|
viewportWidth
|
||||||
|
} = useWorkbenchResponsiveLayout({
|
||||||
|
activeToolOpen: activeToolId !== null,
|
||||||
|
agentPanelOpen: agent.panelOpen,
|
||||||
|
devPanelOpen,
|
||||||
|
collapseAgentPanel: agent.collapsePanel,
|
||||||
|
expandAgentPanel: agent.expandPanel,
|
||||||
|
onClearActiveTool: clearActiveTool
|
||||||
|
});
|
||||||
|
|
||||||
const { mapRef, mapReady, mapError, sourceStatuses, fitNetworkBounds } = useWorkbenchMap({
|
const { mapRef, mapReady, mapError, sourceStatuses, fitNetworkBounds } = useWorkbenchMap({
|
||||||
containerRef: mapContainerRef,
|
containerRef: mapContainerRef,
|
||||||
@@ -379,9 +291,7 @@ export function MapWorkbenchPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getWorkbenchLayerIds(map, layerControlId).forEach((layerId) => {
|
getWorkbenchLayerIds(map, layerControlId).forEach((layerId) => {
|
||||||
if (map.getLayer(layerId)) {
|
map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
||||||
map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,11 +386,10 @@ export function MapWorkbenchPage() {
|
|||||||
setImpactVisible(true);
|
setImpactVisible(true);
|
||||||
setLayerVisibility((current) => ({ ...current, simulation: true }));
|
setLayerVisibility((current) => ({ ...current, simulation: true }));
|
||||||
|
|
||||||
if (mapRef.current && mapReady) {
|
const map = mapRef.current;
|
||||||
WORKBENCH_LAYER_GROUPS.simulation.forEach((layerId) => {
|
if (map && mapReady) {
|
||||||
if (mapRef.current?.getLayer(layerId)) {
|
getWorkbenchLayerIds(map, "simulation").forEach((layerId) => {
|
||||||
mapRef.current.setLayoutProperty(layerId, "visibility", "visible");
|
map.setLayoutProperty(layerId, "visibility", "visible");
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,12 +468,7 @@ export function MapWorkbenchPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setConditionFeedVisible(true);
|
openConditionFeedForViewport();
|
||||||
setActiveToolId(null);
|
|
||||||
if (!isLargeScreen) {
|
|
||||||
setMobileSheet("condition");
|
|
||||||
setMobileSheetSnap("half");
|
|
||||||
}
|
|
||||||
setConditionFocusRequest((current) => ({
|
setConditionFocusRequest((current) => ({
|
||||||
conditionId,
|
conditionId,
|
||||||
requestId: (current?.requestId ?? 0) + 1
|
requestId: (current?.requestId ?? 0) + 1
|
||||||
|
|||||||
@@ -1,4 +1,19 @@
|
|||||||
import { expect, test, type Locator } from "@playwright/test";
|
import { expect, test, type Locator } from "@playwright/test";
|
||||||
|
import { mockAgentApi } from "./support/mock-agent-api";
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await mockAgentApi(page, {
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
session_id: "browser-history-session",
|
||||||
|
title: "城南供水压力复核",
|
||||||
|
created_at: "2026-07-21T09:30:00+08:00",
|
||||||
|
updated_at: "2026-07-21T10:35:00+08:00",
|
||||||
|
run_status: "completed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
type BoundingBox = {
|
type BoundingBox = {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -12,9 +27,7 @@ async function waitForAnimations(locator: Locator) {
|
|||||||
const animations = element
|
const animations = element
|
||||||
.getAnimations({ subtree: true })
|
.getAnimations({ subtree: true })
|
||||||
.filter((animation) => animation.effect?.getTiming().iterations !== Infinity);
|
.filter((animation) => animation.effect?.getTiming().iterations !== Infinity);
|
||||||
await Promise.all(
|
await Promise.all(animations.map((animation) => animation.finished.catch(() => undefined)));
|
||||||
animations.map((animation) => animation.finished.catch(() => undefined))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,9 +116,7 @@ async function expectIntegratedHeader(header: Locator) {
|
|||||||
expect(composition.borderRadius).toBe("0px");
|
expect(composition.borderRadius).toBe("0px");
|
||||||
}
|
}
|
||||||
|
|
||||||
test("desktop Agent header expands into one attached acrylic history surface", async ({
|
test("desktop Agent header expands into one attached acrylic history surface", async ({ page }) => {
|
||||||
page
|
|
||||||
}) => {
|
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
|
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
|
||||||
@@ -194,16 +205,12 @@ test("desktop Agent header expands into one attached acrylic history surface", a
|
|||||||
await expect(history).toBeHidden();
|
await expect(history).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mobile Agent mirrors the desktop floating history surface", async ({
|
test("mobile Agent mirrors the desktop floating history surface", async ({ page }) => {
|
||||||
page
|
|
||||||
}) => {
|
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
||||||
|
|
||||||
const agentPanel = page
|
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true });
|
||||||
.locator('aside[aria-label="Agent 命令面板"]')
|
|
||||||
.filter({ visible: true });
|
|
||||||
await expect(agentPanel).toBeVisible();
|
await expect(agentPanel).toBeVisible();
|
||||||
await waitForAnimations(agentPanel);
|
await waitForAnimations(agentPanel);
|
||||||
const mobileSheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
|
const mobileSheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { mockAgentApi } from "./support/mock-agent-api";
|
||||||
|
|
||||||
test("Agent panel resizes within its responsive 720px workspace limit", async ({ page }) => {
|
test("Agent panel resizes within its responsive 720px workspace limit", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
@@ -71,19 +72,18 @@ test.describe("mobile touch layout", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Temporarily disabled: this regression flow would start a real Agent run.
|
test("mobile alert summary opens the Agent conversation panel", async ({ page }) => {
|
||||||
// Agent analysis must be explicitly triggered by a user, not by automated tests.
|
await mockAgentApi(page);
|
||||||
test.skip("mobile alert summary opens the Agent conversation panel", async ({ page }) => {
|
|
||||||
await page.clock.setFixedTime(new Date("2026-07-21T10:40:00+08:00"));
|
await page.clock.setFixedTime(new Date("2026-07-21T10:40:00+08:00"));
|
||||||
await page.setViewportSize({ width: 375, height: 812 });
|
await page.setViewportSize({ width: 375, height: 812 });
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
await page.getByRole("button", { name: /查看异常处置面板/ }).click();
|
await page.getByRole("button", { name: /查看异常处置面板/ }).click();
|
||||||
// Agent execution must remain an explicit user action.
|
await page.getByRole("button", { name: "工况汇总" }).click();
|
||||||
// await page.getByRole("button", { name: "工况汇总" }).click();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })
|
page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "关闭 Agent 面板" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "关闭 Agent 面板" })).toBeVisible();
|
||||||
|
await expect(page.getByText("已发起工况汇总", { exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ test("submits a custom answer with the actionable id after a later tool placehol
|
|||||||
await mockQuestionApi(page, replies);
|
await mockQuestionApi(page, replies);
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
const prompt = page.getByPlaceholder(/输入调度问题/).filter({ visible: true }).first();
|
const prompt = page
|
||||||
|
.getByPlaceholder(/输入调度问题/)
|
||||||
|
.filter({ visible: true })
|
||||||
|
.first();
|
||||||
await prompt.fill("测试 question 自定义回答");
|
await prompt.fill("测试 question 自定义回答");
|
||||||
await page
|
await page
|
||||||
.getByRole("button", { name: "发送 Agent 指令" })
|
.getByRole("button", { name: "发送 Agent 指令" })
|
||||||
@@ -20,15 +23,17 @@ test("submits a custom answer with the actionable id after a later tool placehol
|
|||||||
await panel.getByPlaceholder("输入自定义回答").fill("分析高峰时段压力");
|
await panel.getByPlaceholder("输入自定义回答").fill("分析高峰时段压力");
|
||||||
await panel.getByRole("button", { name: "提交回答" }).click();
|
await panel.getByRole("button", { name: "提交回答" }).click();
|
||||||
|
|
||||||
await expect.poll(() => replies).toEqual([
|
await expect
|
||||||
{
|
.poll(() => replies)
|
||||||
pathname: "/api/v1/agent/chat/question/question-1/reply",
|
.toEqual([
|
||||||
body: {
|
{
|
||||||
session_id: "question-session",
|
pathname: "/api/v1/agent/chat/question/question-1/reply",
|
||||||
answers: [["分析高峰时段压力"]]
|
body: {
|
||||||
|
session_id: "question-session",
|
||||||
|
answers: [["分析高峰时段压力"]]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]);
|
||||||
]);
|
|
||||||
await expect(panel.getByText("已回答", { exact: true })).toBeVisible();
|
await expect(panel.getByText("已回答", { exact: true })).toBeVisible();
|
||||||
await expect(panel.getByRole("button", { name: "提交回答" })).toHaveCount(0);
|
await expect(panel.getByRole("button", { name: "提交回答" })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
@@ -54,10 +59,7 @@ test("keeps the custom answer control within a 375px Agent sheet", async ({ page
|
|||||||
expect(box!.x + box!.width).toBeLessThanOrEqual(375);
|
expect(box!.x + box!.width).toBeLessThanOrEqual(375);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function mockQuestionApi(
|
async function mockQuestionApi(page: Page, replies: Array<{ pathname: string; body: unknown }>) {
|
||||||
page: Page,
|
|
||||||
replies: Array<{ pathname: string; body: unknown }>
|
|
||||||
) {
|
|
||||||
await page.route("**/api/v1/agent/chat/**", async (route) => {
|
await page.route("**/api/v1/agent/chat/**", async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
const pathname = new URL(request.url()).pathname;
|
const pathname = new URL(request.url()).pathname;
|
||||||
@@ -119,8 +121,8 @@ function createQuestionStream() {
|
|||||||
question: "你想分析哪一部分?",
|
question: "你想分析哪一部分?",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: "雨污混接分析",
|
label: "供水分区压力分析",
|
||||||
description: "检查混接问题"
|
description: "检查分区压力问题"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ test.beforeAll(async () => {
|
|||||||
phase: "planning",
|
phase: "planning",
|
||||||
status: "running",
|
status: "running",
|
||||||
title: "正在分析运行上下文",
|
title: "正在分析运行上下文",
|
||||||
detail: "核对泵站、液位与降雨过程。",
|
detail: "核对泵站、液位与用水过程。",
|
||||||
started_at: Date.now()
|
started_at: Date.now()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -47,8 +47,8 @@ test.beforeAll(async () => {
|
|||||||
id: "inspect-rainfall",
|
id: "inspect-rainfall",
|
||||||
phase: "evidence",
|
phase: "evidence",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
title: "正在核对降雨过程与汇水区响应关系",
|
title: "正在核对用水过程与供水分区响应关系",
|
||||||
detail: "读取最近十二小时分钟级降雨序列,并检查每个汇水区的响应延迟是否处于合理范围。",
|
detail: "读取最近十二小时分钟级流量序列,并检查每个供水分区的响应延迟是否处于合理范围。",
|
||||||
started_at: Date.now()
|
started_at: Date.now()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -74,7 +74,7 @@ test.beforeAll(async () => {
|
|||||||
todos: [
|
todos: [
|
||||||
{ id: "inspect-pump", content: "检查泵站运行状态", status: "in_progress" },
|
{ id: "inspect-pump", content: "检查泵站运行状态", status: "in_progress" },
|
||||||
{ id: "inspect-level", content: "核对液位计连续性", status: "pending" },
|
{ id: "inspect-level", content: "核对液位计连续性", status: "pending" },
|
||||||
{ id: "assess-overflow", content: "判断溢流风险", status: "pending" }
|
{ id: "assess-supply-risk", content: "判断供水风险", status: "pending" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -110,7 +110,7 @@ test.beforeAll(async () => {
|
|||||||
id: "assess-risk",
|
id: "assess-risk",
|
||||||
phase: "analysis",
|
phase: "analysis",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
title: "溢流风险判断完成",
|
title: "供水风险判断完成",
|
||||||
detail: "已完成风险点位排序。",
|
detail: "已完成风险点位排序。",
|
||||||
ended_at: Date.now()
|
ended_at: Date.now()
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,7 @@ test.beforeAll(async () => {
|
|||||||
id: "assess-risk",
|
id: "assess-risk",
|
||||||
phase: "analysis",
|
phase: "analysis",
|
||||||
status: "running",
|
status: "running",
|
||||||
title: "正在判断溢流风险",
|
title: "正在判断供水风险",
|
||||||
detail: "结合流量突变与液位趋势形成判断。",
|
detail: "结合流量突变与液位趋势形成判断。",
|
||||||
started_at: Date.now()
|
started_at: Date.now()
|
||||||
}
|
}
|
||||||
@@ -382,7 +382,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
|||||||
requestAnimationFrame(sample);
|
requestAnimationFrame(sample);
|
||||||
});
|
});
|
||||||
triggerProgressAppend();
|
triggerProgressAppend();
|
||||||
await expect(liveProgress.getByText("正在判断溢流风险", { exact: true })).toBeVisible();
|
await expect(liveProgress.getByText("正在判断供水风险", { exact: true })).toBeVisible();
|
||||||
await expect(liveProgress.locator("li")).toHaveCount(3);
|
await expect(liveProgress.locator("li")).toHaveCount(3);
|
||||||
await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toHaveCount(0);
|
await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toHaveCount(0);
|
||||||
await expect
|
await expect
|
||||||
@@ -450,7 +450,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
|||||||
);
|
);
|
||||||
await expect(expandedLiveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
|
await expect(expandedLiveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
expandedLiveProgress.getByText("正在核对降雨过程与汇水区响应关系", { exact: true })
|
expandedLiveProgress.getByText("正在核对用水过程与供水分区响应关系", { exact: true })
|
||||||
).toHaveCSS("white-space", "normal");
|
).toHaveCSS("white-space", "normal");
|
||||||
await progressToggle.click();
|
await progressToggle.click();
|
||||||
await expect(page.getByRole("list", { name: "最近执行进度" }).locator("li")).toHaveCount(3);
|
await expect(page.getByRole("list", { name: "最近执行进度" }).locator("li")).toHaveCount(3);
|
||||||
@@ -468,7 +468,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
|||||||
await expect(progressToggle.locator("../..")).toContainText("步骤4/4");
|
await expect(progressToggle.locator("../..")).toContainText("步骤4/4");
|
||||||
await expect(page.getByRole("list", { name: "最近执行进度" })).toHaveCount(0);
|
await expect(page.getByRole("list", { name: "最近执行进度" })).toHaveCount(0);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText("溢流风险判断完成", { exact: true }).filter({ visible: true })
|
page.getByText("供水风险判断完成", { exact: true }).filter({ visible: true })
|
||||||
).toHaveCount(0);
|
).toHaveCount(0);
|
||||||
await expect
|
await expect
|
||||||
.poll(async () =>
|
.poll(async () =>
|
||||||
@@ -541,7 +541,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByText("正在分析运行上下文", { exact: true }).filter({ visible: true }).first()
|
page.getByText("正在分析运行上下文", { exact: true }).filter({ visible: true }).first()
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
const expandedLongTitle = expandedProgress.getByText("正在核对降雨过程与汇水区响应关系", {
|
const expandedLongTitle = expandedProgress.getByText("正在核对用水过程与供水分区响应关系", {
|
||||||
exact: true
|
exact: true
|
||||||
});
|
});
|
||||||
await expect(expandedLongTitle).toHaveCSS("white-space", "normal");
|
await expect(expandedLongTitle).toHaveCSS("white-space", "normal");
|
||||||
@@ -562,7 +562,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
|||||||
await expect(latestProgressRegion).toContainText("最近3/3");
|
await expect(latestProgressRegion).toContainText("最近3/3");
|
||||||
triggerProgressAppend();
|
triggerProgressAppend();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("list", { name: "最近执行进度" }).getByText("正在判断溢流风险", { exact: true })
|
page.getByRole("list", { name: "最近执行进度" }).getByText("正在判断供水风险", { exact: true })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await latestProgressToggle.click();
|
await latestProgressToggle.click();
|
||||||
await expect(latestProgressRegion).toContainText("全部4/4");
|
await expect(latestProgressRegion).toContainText("全部4/4");
|
||||||
@@ -570,7 +570,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
|||||||
|
|
||||||
await expect(latestProgressRegion).toContainText("全部4/4", { timeout: 10_000 });
|
await expect(latestProgressRegion).toContainText("全部4/4", { timeout: 10_000 });
|
||||||
await expect(latestExpandedProgress).toBeVisible();
|
await expect(latestExpandedProgress).toBeVisible();
|
||||||
await expect(latestExpandedProgress.getByText("溢流风险判断完成", { exact: true })).toBeVisible();
|
await expect(latestExpandedProgress.getByText("供水风险判断完成", { exact: true })).toBeVisible();
|
||||||
|
|
||||||
const conversationScroll = page.locator(".agent-conversation-scroll");
|
const conversationScroll = page.locator(".agent-conversation-scroll");
|
||||||
await conversationScroll.hover();
|
await conversationScroll.hover();
|
||||||
@@ -673,7 +673,7 @@ function createLongMarkdownResponse() {
|
|||||||
"当前管网运行状态总体稳定,但部分区域存在水位持续升高、流量突变和监测数据缺失。需要结合实时工况核对监测点连续性。\n\n";
|
"当前管网运行状态总体稳定,但部分区域存在水位持续升高、流量突变和监测数据缺失。需要结合实时工况核对监测点连续性。\n\n";
|
||||||
content += "- 检查泵站运行状态与实时流量。\n";
|
content += "- 检查泵站运行状态与实时流量。\n";
|
||||||
content += "- 核对液位计连续性和异常峰值。\n";
|
content += "- 核对液位计连续性和异常峰值。\n";
|
||||||
content += "- 结合降雨过程判断溢流风险。\n\n";
|
content += "- 结合用水过程判断供水风险。\n\n";
|
||||||
}
|
}
|
||||||
return `${content}建议优先处置高风险点位,并持续跟踪后续变化。`;
|
return `${content}建议优先处置高风险点位,并持续跟踪后续变化。`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
type MockAgentApiOptions = {
|
||||||
|
sessions?: Array<Record<string, unknown>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}) {
|
||||||
|
await page.route("**/api/v1/agent/chat/**", async (route) => {
|
||||||
|
const pathname = new URL(route.request().url()).pathname;
|
||||||
|
|
||||||
|
if (pathname.endsWith("/stream")) {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"X-Vercel-AI-UI-Message-Stream": "v1"
|
||||||
|
},
|
||||||
|
body: [
|
||||||
|
{ type: "start", messageId: "assistant-browser-test" },
|
||||||
|
{ type: "text-start", id: "answer" },
|
||||||
|
{ type: "text-delta", id: "answer", delta: "已收到工况汇总请求。" },
|
||||||
|
{ type: "text-end", id: "answer" },
|
||||||
|
{ type: "finish", finishReason: "stop" }
|
||||||
|
]
|
||||||
|
.map((part) => `data: ${JSON.stringify(part)}\n\n`)
|
||||||
|
.join("")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.endsWith("/sessions")) {
|
||||||
|
await route.fulfill({ json: { sessions: options.sessions ?? [] } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.endsWith("/models")) {
|
||||||
|
await route.fulfill({
|
||||||
|
json: {
|
||||||
|
default_model: "test/model",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "test/model",
|
||||||
|
label: "测试模型",
|
||||||
|
description: "Playwright 浏览器测试",
|
||||||
|
icon: "bolt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.endsWith("/ui-registry")) {
|
||||||
|
await route.fulfill({
|
||||||
|
json: {
|
||||||
|
schema_version: "agent-ui-registry@1",
|
||||||
|
chart_grammars: [],
|
||||||
|
components: [],
|
||||||
|
actions: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.endsWith("/frontend-action-registry")) {
|
||||||
|
await route.fulfill({
|
||||||
|
json: {
|
||||||
|
schema_version: "frontend-action-registry@1",
|
||||||
|
actions: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await route.fulfill({ json: {} });
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,33 +2,30 @@ import { expect, test } from "@playwright/test";
|
|||||||
|
|
||||||
test("opening Agent and condition panels does not move the map camera", async ({ page }) => {
|
test("opening Agent and condition panels does not move the map camera", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
await page.waitForFunction(() =>
|
await page.waitForFunction(
|
||||||
Boolean(window.__waterNetworkMap?.getLayer("scada-hit"))
|
() =>
|
||||||
|
Boolean(window.__waterNetworkMap?.getLayer("scada-hit")) &&
|
||||||
|
!window.__waterNetworkMap?.isMoving()
|
||||||
);
|
);
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const map = window.__waterNetworkMap;
|
const map = window.__waterNetworkMap;
|
||||||
document.documentElement.dataset.panelCameraMoveCount = "0";
|
document.documentElement.dataset.panelCameraMoveCount = "0";
|
||||||
map?.on("movestart", () => {
|
map?.on("movestart", () => {
|
||||||
const currentCount = Number(
|
const currentCount = Number(document.documentElement.dataset.panelCameraMoveCount ?? "0");
|
||||||
document.documentElement.dataset.panelCameraMoveCount ?? "0"
|
|
||||||
);
|
|
||||||
document.documentElement.dataset.panelCameraMoveCount = String(currentCount + 1);
|
document.documentElement.dataset.panelCameraMoveCount = String(currentCount + 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByRole("button", { name: "折叠 Agent 面板" }).click();
|
await page.getByRole("button", { name: "折叠 Agent 面板" }).click();
|
||||||
await page.waitForTimeout(250);
|
await expect(page.locator('aside[aria-label="Agent 折叠栏"]')).toBeVisible();
|
||||||
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
||||||
await page.waitForTimeout(250);
|
await expect(page.locator('aside[aria-label="Agent 命令面板"]')).toBeVisible();
|
||||||
|
|
||||||
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||||
await conditionPanel.getByRole("button", { name: "展开工况任务" }).click();
|
await conditionPanel.getByRole("button", { name: "展开工况任务" }).click();
|
||||||
await page.waitForTimeout(250);
|
await expect(conditionPanel).toHaveCSS("width", "880px");
|
||||||
|
|
||||||
expect(
|
expect(await page.evaluate(() => document.documentElement.dataset.panelCameraMoveCount)).toBe(
|
||||||
await page.evaluate(
|
"0"
|
||||||
() => document.documentElement.dataset.panelCameraMoveCount
|
);
|
||||||
)
|
|
||||||
).toBe("0");
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -176,14 +176,17 @@ async function openWorkbench(page: Page) {
|
|||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
await document.fonts.ready;
|
await document.fonts.ready;
|
||||||
});
|
});
|
||||||
await page.waitForTimeout(350);
|
await page.waitForFunction(() => Boolean(window.__waterNetworkMap?.getLayer("scada-hit")));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectBasemap(page: Page, label: "浅色" | "影像") {
|
async function selectBasemap(page: Page, label: "浅色" | "影像") {
|
||||||
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
||||||
await page.getByRole("button", { name: label, exact: true }).click();
|
await page.getByRole("button", { name: label, exact: true }).click();
|
||||||
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
||||||
await page.waitForTimeout(220);
|
await expect(page.locator("main")).toHaveAttribute(
|
||||||
|
"data-basemap-tone",
|
||||||
|
label === "影像" ? "satellite" : "light"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expectDesktopFloatingGeometry(page: Page) {
|
async function expectDesktopFloatingGeometry(page: Page) {
|
||||||
@@ -243,9 +246,7 @@ async function expectAcrylicAlphas(
|
|||||||
const alphas = await page.evaluate(() => {
|
const alphas = await page.evaluate(() => {
|
||||||
return {
|
return {
|
||||||
navigation: alphaOf(document.querySelector(".acrylic-navigation")),
|
navigation: alphaOf(document.querySelector(".acrylic-navigation")),
|
||||||
agentPanel: alphaOf(
|
agentPanel: alphaOf(document.querySelector('aside[aria-label="Agent 命令面板"]')),
|
||||||
document.querySelector('aside[aria-label="Agent 命令面板"]')
|
|
||||||
),
|
|
||||||
conditionPanel: alphaOf(document.querySelector(".scheduled-feed-panel-shell")),
|
conditionPanel: alphaOf(document.querySelector(".scheduled-feed-panel-shell")),
|
||||||
control: alphaOf(document.querySelector(".acrylic-control"))
|
control: alphaOf(document.querySelector(".acrylic-control"))
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user