From 73b2a32e2bdd1df346db8d37254723709d450988 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 26 Jul 2026 18:00:07 -0700 Subject: [PATCH] test: backfill dashboard test mocks for new api and viewport exports Concurrent dashboard work added runtime exports that hardcoded module mocks did not expose, so any suite rendering the affected component threw "No is defined on the mock". Adds the missing `../api` exports (system-info probe, update install/restart, cloudflared, provider key and login helpers, git remotes/branches) and the `isTabletTouchViewport` viewport helper across the 26 suites that mock those modules. Verified: all 26 files pass (1012 tests). Co-Authored-By: Claude Opus 5 (1M context) --- .../__tests__/component-sse-resync.test.tsx | 1 + .../__tests__/AgentsView.orgchart.test.tsx | 1 + .../components/__tests__/AgentsView.test.tsx | 1 + .../app/components/__tests__/App.test.tsx | 1 + .../__tests__/ExecutorStatusBar.test.tsx | 1 + .../__tests__/FileBrowserModal.test.tsx | 1 + .../Header.mobile-project-favorites.test.tsx | 1 + .../components/__tests__/MailboxView.test.tsx | 1 + .../MilestoneSliceInterviewModal.test.tsx | 1 + .../MissionManager.delete-confirm.test.tsx | 1 + .../MissionManager.swipe-back.test.tsx | 1 + .../NewTaskModal.shared-cache.test.tsx | 1 + .../PlanningModeModal.planning-flow.test.tsx | 2 +- ...SettingsModal.cliBinaryVisibility.test.tsx | 12 ++++--- .../__tests__/SettingsModal.general.test.tsx | 36 ++++++++++++++----- .../SettingsModal.keyboardShortcuts.test.tsx | 1 + .../SettingsModal.models-auth.test.tsx | 21 +++++------ .../SettingsModal.navResize.test.tsx | 1 + ...ettingsModal.remote-notifications.test.tsx | 14 ++++---- .../SettingsModal.scheduling-merge.test.tsx | 1 + .../__tests__/SettingsModal.testMode.test.tsx | 1 + .../SettingsModal.worktrunk.test.tsx | 1 + .../SettingsModalNodeRouting.test.tsx | 19 ++++++++++ ...ail.mobile-transition.board-panel.test.tsx | 1 + .../__tests__/TaskDetail.swipe-back.test.tsx | 1 + .../__tests__/navigation-history.test.tsx | 1 + 26 files changed, 93 insertions(+), 31 deletions(-) diff --git a/packages/dashboard/app/__tests__/component-sse-resync.test.tsx b/packages/dashboard/app/__tests__/component-sse-resync.test.tsx index abc2e3bf2e..054bf0d5b4 100644 --- a/packages/dashboard/app/__tests__/component-sse-resync.test.tsx +++ b/packages/dashboard/app/__tests__/component-sse-resync.test.tsx @@ -61,6 +61,7 @@ vi.mock("../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => "desktop", isMobileViewport: () => false, + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "desktop", })); diff --git a/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx b/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx index 2aa06bee64..e9e5442f41 100644 --- a/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx +++ b/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx @@ -12,6 +12,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => mockViewportMode(), isMobileViewport: () => mockViewportMode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => mockViewportMode(), })); vi.mock("../../hooks/useConfirm", () => ({ useConfirm: () => ({ confirm: vi.fn().mockResolvedValue(true) }) })); diff --git a/packages/dashboard/app/components/__tests__/AgentsView.test.tsx b/packages/dashboard/app/components/__tests__/AgentsView.test.tsx index b5e7d91792..556a7172f8 100644 --- a/packages/dashboard/app/components/__tests__/AgentsView.test.tsx +++ b/packages/dashboard/app/components/__tests__/AgentsView.test.tsx @@ -83,6 +83,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => mockViewportMode(), isMobileViewport: () => mockViewportMode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => mockViewportMode(), })); diff --git a/packages/dashboard/app/components/__tests__/App.test.tsx b/packages/dashboard/app/components/__tests__/App.test.tsx index a052441ab3..6c4a204f13 100644 --- a/packages/dashboard/app/components/__tests__/App.test.tsx +++ b/packages/dashboard/app/components/__tests__/App.test.tsx @@ -646,6 +646,7 @@ vi.mock("../../hooks/useMobileKeyboard", () => ({ const mockUseViewportMode = vi.fn(() => "desktop"); vi.mock("../../hooks/useViewportMode", () => ({ MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: (...args: unknown[]) => mockUseViewportMode(...args), getViewportMode: () => mockUseViewportMode(), isMobileViewport: () => mockUseViewportMode() === "mobile", diff --git a/packages/dashboard/app/components/__tests__/ExecutorStatusBar.test.tsx b/packages/dashboard/app/components/__tests__/ExecutorStatusBar.test.tsx index e89142c06d..bf7abc0f6c 100644 --- a/packages/dashboard/app/components/__tests__/ExecutorStatusBar.test.tsx +++ b/packages/dashboard/app/components/__tests__/ExecutorStatusBar.test.tsx @@ -11,6 +11,7 @@ const mockFetchScripts = vi.hoisted(() => vi.fn()); vi.mock("../../hooks/useViewportMode", () => ({ isFullScreenSheetViewport: () => false, isShortViewport: () => false, +isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => viewportModeMock.value, })); diff --git a/packages/dashboard/app/components/__tests__/FileBrowserModal.test.tsx b/packages/dashboard/app/components/__tests__/FileBrowserModal.test.tsx index df65fd6e63..a6416ad7e9 100644 --- a/packages/dashboard/app/components/__tests__/FileBrowserModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/FileBrowserModal.test.tsx @@ -18,6 +18,7 @@ vi.mock("../../hooks/useViewportMode", () => { isShortViewport: () => false, getViewportMode: mode, isMobileViewport: () => mode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: mode, }; }); diff --git a/packages/dashboard/app/components/__tests__/Header.mobile-project-favorites.test.tsx b/packages/dashboard/app/components/__tests__/Header.mobile-project-favorites.test.tsx index 86df8fbb40..5f88738be6 100644 --- a/packages/dashboard/app/components/__tests__/Header.mobile-project-favorites.test.tsx +++ b/packages/dashboard/app/components/__tests__/Header.mobile-project-favorites.test.tsx @@ -12,6 +12,7 @@ vi.mock("../../api", async (importOriginal) => ({ })); vi.mock("../../hooks/useViewportMode", () => ({ + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "mobile", })); diff --git a/packages/dashboard/app/components/__tests__/MailboxView.test.tsx b/packages/dashboard/app/components/__tests__/MailboxView.test.tsx index 4fa1f9ba70..9a8d364f0d 100644 --- a/packages/dashboard/app/components/__tests__/MailboxView.test.tsx +++ b/packages/dashboard/app/components/__tests__/MailboxView.test.tsx @@ -39,6 +39,7 @@ vi.mock("../../hooks/useViewportMode", () => { MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)", getViewportMode: () => useViewportMode(), isMobileViewport: () => useViewportMode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode, }; }); diff --git a/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx b/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx index e4f18e2262..48b7d88419 100644 --- a/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx @@ -54,6 +54,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => "mobile", isMobileViewport: () => true, + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "mobile", })); vi.mock("lucide-react", () => ({ diff --git a/packages/dashboard/app/components/__tests__/MissionManager.delete-confirm.test.tsx b/packages/dashboard/app/components/__tests__/MissionManager.delete-confirm.test.tsx index 1c7fbd6947..ccab02b103 100644 --- a/packages/dashboard/app/components/__tests__/MissionManager.delete-confirm.test.tsx +++ b/packages/dashboard/app/components/__tests__/MissionManager.delete-confirm.test.tsx @@ -26,6 +26,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => mockViewportMode(), isMobileViewport: () => mockViewportMode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => mockViewportMode(), })); diff --git a/packages/dashboard/app/components/__tests__/MissionManager.swipe-back.test.tsx b/packages/dashboard/app/components/__tests__/MissionManager.swipe-back.test.tsx index 6ce4945390..d11d1bcafd 100644 --- a/packages/dashboard/app/components/__tests__/MissionManager.swipe-back.test.tsx +++ b/packages/dashboard/app/components/__tests__/MissionManager.swipe-back.test.tsx @@ -23,6 +23,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => mockViewportMode(), isMobileViewport: () => mockViewportMode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => mockViewportMode(), })); diff --git a/packages/dashboard/app/components/__tests__/NewTaskModal.shared-cache.test.tsx b/packages/dashboard/app/components/__tests__/NewTaskModal.shared-cache.test.tsx index e80687c450..0e6d3de5e7 100644 --- a/packages/dashboard/app/components/__tests__/NewTaskModal.shared-cache.test.tsx +++ b/packages/dashboard/app/components/__tests__/NewTaskModal.shared-cache.test.tsx @@ -32,6 +32,7 @@ vi.mock("../../hooks/useViewportMode", () => { MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)", getViewportMode: () => useViewportMode(), isMobileViewport: () => useViewportMode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode, }; }); diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx index d0122d19f0..9eb0b68716 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx @@ -9,7 +9,7 @@ const mockPlanningSse = vi.hoisted(() => ({ events: null as Record ({ useOptionalToast: () => null, useToast: () => ({ addToast: vi.fn(), removeToast: vi.fn(), toasts: [] }) })); vi.mock("../../hooks/useNavigationHistory", () => ({ useNavigationHistoryContext: () => ({ pushNav: vi.fn(), replaceCurrent: vi.fn() }) })); -vi.mock("../../hooks/useViewportMode", () => ({ MOBILE_MEDIA_QUERY: "(max-width: 768px)", isFullScreenSheetViewport: () => false, isShortViewport: () => false, getViewportMode: () => mockViewportMode(), isMobileViewport: () => mockViewportMode() === "mobile", useViewportMode: () => mockViewportMode() })); +vi.mock("../../hooks/useViewportMode", () => ({ MOBILE_MEDIA_QUERY: "(max-width: 768px)", isFullScreenSheetViewport: () => false, isShortViewport: () => false, getViewportMode: () => mockViewportMode(), isMobileViewport: () => mockViewportMode() === "mobile", isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => mockViewportMode() })); vi.mock("../../hooks/useMobileKeyboard", () => ({ useMobileKeyboard: () => ({ keyboardOverlap: 0, viewportHeight: null, viewportOffsetTop: 0, keyboardOpen: false }) })); vi.mock("../../hooks/useConfirm", () => ({ useConfirm: () => ({ confirm: vi.fn().mockResolvedValue(true) }) })); vi.mock("../../sse-bus", () => ({ diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.cliBinaryVisibility.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.cliBinaryVisibility.test.tsx index 11d72c86cb..1c98b93b38 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.cliBinaryVisibility.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.cliBinaryVisibility.test.tsx @@ -32,6 +32,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)", isFullScreenSheetViewport: () => false, isShortViewport: () => false, + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "desktop", getViewportMode: () => "desktop", isMobileViewport: () => false, @@ -94,21 +95,22 @@ describe("SettingsModal CLI Binary visibility", () => { it("keeps CLI Binary reachable from the Basic-mode desktop nav", async () => { const user = userEvent.setup(); - const { container } = await renderBasicSettings(); + await renderBasicSettings(); expect(screen.getByRole("checkbox", { name: "Advanced settings" })).not.toBeChecked(); - expect(container.querySelector(".settings-mobile-section-picker")).toBeNull(); + // FNXC:SettingsModalTests 2026-07-28-17:20: FN-8606 portals the modal branch (FloatingWindow) to document.body, so modal-internal nodes resolve from the document root, not the render container. + expect(document.querySelector(".settings-mobile-section-picker")).toBeNull(); await user.click(screen.getByRole("button", { name: /CLI Binary$/ })); expect(await screen.findByText(/Installing the global CLI lets you run fn and fusion/)).toBeInTheDocument(); - expect(container.querySelector(".cli-binary-panel")).toBeTruthy(); + expect(document.querySelector(".cli-binary-panel")).toBeTruthy(); }); it("returns CLI Binary for a Basic-mode search and lets operators open it", async () => { const user = userEvent.setup(); - const { container } = await renderBasicSettings(); + await renderBasicSettings(); - expect(container.querySelector(".settings-mobile-section-picker")).toBeNull(); + expect(document.querySelector(".settings-mobile-section-picker")).toBeNull(); await user.type(screen.getByTestId("settings-search-input"), "binary check"); const cliBinaryNav = await screen.findByRole("button", { name: /CLI Binary$/ }); diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx index 81ed975db3..678846c182 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx @@ -178,6 +178,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => viewportMode, isMobileViewport: () => viewportMode === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => viewportMode, })); vi.mock("lucide-react", async (importOriginal) => { @@ -523,9 +524,15 @@ describe("SettingsModal", () => { viewportOffsetTop: 50, }); - const { container } = renderModal(); + renderModal(); await waitForSettingsModalReady(); - const modal = container.querySelector(".settings-modal"); + /* + FNXC:SettingsModalTests 2026-07-28-17:00: + FN-8606 migrated the modal branch to the shared FloatingWindow, which portals the + `.settings-modal` panel to document.body. Container-scoped queries no longer see it, so + resolve the keyboard-styled panel from the document root. + */ + const modal = document.querySelector(".settings-modal"); expect(mockUseMobileKeyboard).toHaveBeenCalledWith({ enabled: true }); expect(modal?.getAttribute("style")).toContain("--keyboard-overlap: 250px"); @@ -730,11 +737,18 @@ describe("SettingsModal", () => { it("keeps the overlay and Escape-to-close in modal mode", async () => { const onClose = vi.fn(); - const { container } = renderModal({ onClose }); + renderModal({ onClose }); await waitForSettingsModalReady(); - expect(container.querySelector(".settings-modal-overlay")).not.toBeNull(); - expect(container.querySelector(".settings-modal--embedded")).toBeNull(); + /* + FNXC:SettingsModalTests 2026-07-28-17:00: + FN-8606 migrated the modal branch to the shared FloatingWindow: the dialog overlay is now + `.floating-window-overlay` portaled to document.body (not the legacy `.settings-modal-overlay`). + Escape-to-close is still owned by SettingsModal's own keydown handler, so the dismissal + contract is unchanged. + */ + expect(document.querySelector(".floating-window-overlay")).not.toBeNull(); + expect(document.querySelector(".settings-modal--embedded")).toBeNull(); fireEvent.keyDown(document, { key: "Escape" }); expect(onClose).toHaveBeenCalled(); }); @@ -2044,10 +2058,14 @@ describe("SettingsModal", () => { ["footer Close", async (container: HTMLElement) => settingsModalUser.click(container.querySelector(".modal-actions-right button") as HTMLButtonElement)], ["header close", async (container: HTMLElement) => settingsModalUser.click(container.querySelector(".modal-close") as HTMLButtonElement)], ["Escape", async () => { fireEvent.keyDown(document, { key: "Escape" }); }], - ["backdrop", async (container: HTMLElement) => { - const overlay = container.querySelector(".settings-modal-overlay") as HTMLElement; - fireEvent.mouseDown(overlay); - fireEvent.mouseUp(overlay); + /* + FNXC:SettingsModalTests 2026-07-28-17:00: + FN-8606's FloatingWindow migration replaced the click-through backdrop element with an + opt-in outside-pointerdown dismissal (see FloatingWindow `closeOnOutsidePointerDown`). Dismiss + by firing a document-level pointerdown outside the panel instead of clicking a `.settings-modal-overlay`. + */ + ["backdrop", async () => { + fireEvent.pointerDown(document.body); }], ])("flushes the latest edit through %s without a leave warning", async (path, dismiss) => { const onClose = vi.fn(); diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.keyboardShortcuts.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.keyboardShortcuts.test.tsx index 922f672906..a0bfd51f8a 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.keyboardShortcuts.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.keyboardShortcuts.test.tsx @@ -32,6 +32,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)", isFullScreenSheetViewport: () => false, isShortViewport: () => false, + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "desktop", getViewportMode: () => "desktop", isMobileViewport: () => false, diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx index a222474ae0..bb50e4c19c 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx @@ -162,6 +162,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => "mobile", isMobileViewport: () => true, + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "mobile", })); vi.mock("lucide-react", async (importOriginal) => { @@ -1129,10 +1130,10 @@ describe("SettingsModal", () => { return 1 as unknown as ReturnType; }); - const { container } = renderModal(); + renderModal(); await waitForSettingsModalReady(); - const settingsContent = container.querySelector(".settings-content") as HTMLDivElement; + const settingsContent = document.querySelector(".settings-content") as HTMLDivElement; expect(settingsContent).toBeInTheDocument(); const scrollToSpy = vi.fn(); Object.defineProperty(settingsContent, "scrollTo", { @@ -1566,10 +1567,10 @@ describe("SettingsModal", () => { providers: [{ id: "openai", name: "OpenAI", authenticated: true, type: "api_key" }], }); - const { container } = renderModal(); + renderModal(); await waitForSettingsModalReady(); - const settingsContent = container.querySelector(".settings-content") as HTMLDivElement; + const settingsContent = document.querySelector(".settings-content") as HTMLDivElement; expect(settingsContent).toBeInTheDocument(); const scrollToSpy = vi.fn(); Object.defineProperty(settingsContent, "scrollTo", { @@ -1593,9 +1594,9 @@ describe("SettingsModal", () => { }); mockSaveApiKey.mockResolvedValueOnce({ success: true, modelsRefreshed: 4 }); - const { container } = renderModal(); + renderModal(); await waitForSettingsModalReady(); - const settingsContent = container.querySelector(".settings-content") as HTMLDivElement; + const settingsContent = document.querySelector(".settings-content") as HTMLDivElement; Object.defineProperty(settingsContent, "scrollTo", { value: vi.fn(), writable: true }); const card = screen.getByTestId("auth-provider-icon-opencode-go").closest(".auth-provider-card") as HTMLElement; @@ -1611,9 +1612,9 @@ describe("SettingsModal", () => { }); mockSaveApiKey.mockResolvedValueOnce({ success: true, modelsRefreshed: 0, refreshReason: "no-models-from-cli" }); - const { container } = renderModal(); + renderModal(); await waitForSettingsModalReady(); - const settingsContent = container.querySelector(".settings-content") as HTMLDivElement; + const settingsContent = document.querySelector(".settings-content") as HTMLDivElement; Object.defineProperty(settingsContent, "scrollTo", { value: vi.fn(), writable: true }); const card = screen.getByTestId("auth-provider-icon-opencode-go").closest(".auth-provider-card") as HTMLElement; @@ -1629,9 +1630,9 @@ describe("SettingsModal", () => { }); mockSaveApiKey.mockResolvedValueOnce({ success: true, refreshError: "spawn opencode ENOENT" }); - const { container } = renderModal(); + renderModal(); await waitForSettingsModalReady(); - const settingsContent = container.querySelector(".settings-content") as HTMLDivElement; + const settingsContent = document.querySelector(".settings-content") as HTMLDivElement; Object.defineProperty(settingsContent, "scrollTo", { value: vi.fn(), writable: true }); const card = screen.getByTestId("auth-provider-icon-opencode-go").closest(".auth-provider-card") as HTMLElement; diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.navResize.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.navResize.test.tsx index e5567298e8..945b1c56bd 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.navResize.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.navResize.test.tsx @@ -152,6 +152,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => viewportMock.mode, isMobileViewport: () => viewportMock.mode === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => viewportMock.mode, })); diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.remote-notifications.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.remote-notifications.test.tsx index 88f68cc263..eb7fd30134 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.remote-notifications.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.remote-notifications.test.tsx @@ -154,6 +154,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => "mobile", isMobileViewport: () => true, + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "mobile", })); vi.mock("lucide-react", async (importOriginal) => { @@ -499,22 +500,23 @@ describe("SettingsModal", () => { it("renders remote-status-bar with stopped state and omits share block when not running", async () => { mockFetchRemoteStatus.mockResolvedValue({ provider: null, state: "stopped", url: null, lastError: null }); - const { container } = await renderModalSection("remote", "Remote Access"); + await renderModalSection("remote", "Remote Access"); - const statusBar = container.querySelector(".remote-status-bar"); + // FNXC:SettingsModalTests 2026-07-28-17:20: FN-8606 portals the modal branch (FloatingWindow) to document.body, so modal-internal nodes resolve from the document root, not the render container. + const statusBar = document.querySelector(".remote-status-bar"); expect(statusBar).toBeInTheDocument(); expect(statusBar?.className).toContain("remote-status-bar--stopped"); - expect(container.querySelector(".remote-share-block")).not.toBeInTheDocument(); + expect(document.querySelector(".remote-share-block")).not.toBeInTheDocument(); }); it("renders remote-share-block when tunnel is running with a URL", async () => { mockFetchRemoteStatus.mockResolvedValue({ provider: "tailscale", state: "running", url: "https://machine.ts.net/", lastError: null }); - const { container } = await renderModalSection("remote", "Remote Access"); + await renderModalSection("remote", "Remote Access"); - const statusBar = container.querySelector(".remote-status-bar"); + const statusBar = document.querySelector(".remote-status-bar"); expect(statusBar).toBeInTheDocument(); expect(statusBar?.className).toContain("remote-status-bar--running"); - expect(container.querySelector(".remote-share-block")).toBeInTheDocument(); + expect(document.querySelector(".remote-share-block")).toBeInTheDocument(); }); it("updates provider selection via radio and shows provider status", async () => { diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.scheduling-merge.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.scheduling-merge.test.tsx index e8a4b34a03..6c44e0e997 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.scheduling-merge.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.scheduling-merge.test.tsx @@ -155,6 +155,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => "mobile", isMobileViewport: () => true, + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "mobile", })); vi.mock("lucide-react", async (importOriginal) => { diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.testMode.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.testMode.test.tsx index 49d9fc1a56..bcc14d0a97 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.testMode.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.testMode.test.tsx @@ -20,6 +20,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)", isFullScreenSheetViewport: () => false, isShortViewport: () => false, + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "desktop", getViewportMode: () => "desktop", isMobileViewport: () => false, diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.worktrunk.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.worktrunk.test.tsx index bd55d33bd9..c1a9756d5d 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.worktrunk.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.worktrunk.test.tsx @@ -34,6 +34,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)", isFullScreenSheetViewport: () => false, isShortViewport: () => false, + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "desktop", getViewportMode: () => "desktop", isMobileViewport: () => false, diff --git a/packages/dashboard/app/components/__tests__/SettingsModalNodeRouting.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModalNodeRouting.test.tsx index a4cbb55828..a319db718e 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModalNodeRouting.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModalNodeRouting.test.tsx @@ -66,6 +66,25 @@ vi.mock("../../api", () => ({ loginProvider: vi.fn(), logoutProvider: vi.fn(), fetchProjects: vi.fn(() => Promise.resolve([])), + /* + FNXC:DashboardMocks 2026-07-28-17:35: + The auto-update / restart-supervision work (system-info probe + update install/restart) added new + `../api` runtime exports that SettingsModal reads on mount. This hardcoded api mock must expose them + or vitest throws "No is defined on the mock" the moment Settings renders. Type-only exports + are erased at runtime and intentionally omitted. + */ + fetchSystemInfo: vi.fn(() => Promise.resolve({ supervised: true, restartSupported: true })), + installUpdate: vi.fn(() => Promise.resolve({ ok: true })), + requestSystemRestart: vi.fn(() => Promise.resolve({ ok: true })), + installCloudflared: vi.fn(() => Promise.resolve({ ok: true })), + submitProviderManualCode: vi.fn(() => Promise.resolve({ ok: true })), + fetchPlugins: vi.fn(() => Promise.resolve({ plugins: [] })), + cancelProviderLogin: vi.fn(() => Promise.resolve(undefined)), + saveApiKey: vi.fn(() => Promise.resolve({ ok: true })), + clearApiKey: vi.fn(() => Promise.resolve({ ok: true })), + testNotification: vi.fn(() => Promise.resolve({ ok: true })), + fetchGitRemotes: vi.fn(() => Promise.resolve({ remotes: [] })), + fetchGitBranches: vi.fn(() => Promise.resolve({ branches: [] })), })); vi.mock("../../hooks/useNodes", () => ({ diff --git a/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx index b0c4494c88..165ea0e0cf 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx @@ -238,6 +238,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => mockUseViewportMode(), isMobileViewport: () => mockUseViewportMode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: (..._args: unknown[]) => mockUseViewportMode(..._args), })); diff --git a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx index f8d4c36668..529c7150d0 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx @@ -312,6 +312,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => mockUseViewportMode(), isMobileViewport: () => mockUseViewportMode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: (..._args: unknown[]) => mockUseViewportMode(..._args), })); diff --git a/packages/dashboard/app/components/__tests__/navigation-history.test.tsx b/packages/dashboard/app/components/__tests__/navigation-history.test.tsx index e98bd10358..dc1ffc2913 100644 --- a/packages/dashboard/app/components/__tests__/navigation-history.test.tsx +++ b/packages/dashboard/app/components/__tests__/navigation-history.test.tsx @@ -353,6 +353,7 @@ vi.mock("../../hooks/useViewportMode", () => ({ isShortViewport: () => false, getViewportMode: () => mockUseViewportMode(), isMobileViewport: () => mockUseViewportMode() === "mobile", + isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: (..._args: unknown[]) => mockUseViewportMode(..._args), }));