refactor: simplify workbench layout and browser tests

This commit is contained in:
2026-08-18 17:06:42 +08:00
parent bdf4436899
commit bdd5eff776
12 changed files with 448 additions and 201 deletions
@@ -168,9 +168,6 @@ export function useWorkbenchMap({
const resizeMap = () => map.resize();
window.addEventListener("resize", resizeMap);
window.setTimeout(resizeMap, 0);
window.setTimeout(resizeMap, 300);
window.setTimeout(resizeMap, 1000);
map.on("sourcedata", (event) => {
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
};
}