diff --git a/frontend/src/pages/.pi-map.index.md b/frontend/src/pages/.pi-map.index.md index 127a8aa..2845161 100644 --- a/frontend/src/pages/.pi-map.index.md +++ b/frontend/src/pages/.pi-map.index.md @@ -2,7 +2,7 @@ dir: frontend/src/pages ## role -Top-level page components that render the main UI views for dashboards, services, and settings in the application. +Top-level page components that render the application's primary UI views including dashboards, service management, and settings. ## parent index: frontend/src/.pi-map.index.md map: frontend/src/.pi-map.md diff --git a/frontend/src/pages/.pi-map.md b/frontend/src/pages/.pi-map.md index 24dd913..88ed91a 100644 --- a/frontend/src/pages/.pi-map.md +++ b/frontend/src/pages/.pi-map.md @@ -4,18 +4,18 @@ dir: frontend/src/pages index: frontend/src/pages/.pi-map.index.md ## role -Top-level page components that render the main UI views for dashboards, services, and settings in the application. +Top-level page components that render the application's primary UI views including dashboards, service management, and settings. ## files - Dashboard.tsx | This file renders the main dashboard page, displaying widget instances grouped by category and user-created shortcuts with full CRUD dialog management. | exp: func:Dashboard(), call:useNavigate, call:useDashboardShortcuts, call:useSaveDashboardShortcut, call:useDeleteDashboardShortcut, call:useState, call:emptyShortcut, call:useWidgetInstances, call:useWidgetReferences, call:useServiceInstances, call:useIsMobile, call:useMemo, call:widgetReferences .filter((r) => r.widget.enabled) .map, call:[...widgetInstances, ...refs] .filter((w) => w.enabled) .sort, call:widgetReferences.map, call:useDetachWidgetReference, call:groupWidgetsBySection, call:setShortcutDraft, call:setShortcutDialogOpen, call:saveShortcut.mutateAsync, call:navigate, call:setWidgetDialogOpen, call:shortcuts.map, call:shortcutHref, call:window.open, call:openEditShortcut, call:setDeleteShortcutId, call:setEditWidgetId, call:widgetReferences.find, call:detachRef.mutate, call:visibleWidgets.map, call:referencedWidgetIds.has, call:Boolean, call:deleteShortcut.mutate | dep: react, react-router-dom, lucide-react, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/switch, ../hooks/useDashboard, ../hooks/useWidgets, ../hooks/useServices, ../hooks/useIsMobile, ../types, ../components/SectionCard, ../components/ConfirmDialog, ../components/DialogFooter, ../components/WidgetInstance, ../components/WidgetConfigDialog, @/components/ui/* - NamedDashboardPage.tsx | Renders a named dashboard page that displays pinned service links and configurable widgets based on a slug-based lookup. | exp: func:NamedDashboardPage(), call:useParams, call:useDashboardBySlug, call:useWidgetReferences, call:useState, call:useMemo, call:parseItems, call:widgetRefs .filter((r) => r.widget.enabled) .map((r) => r.widget) .sort, call:setConfigOpen, call:visibleWidgets.map, call:setEditWidgetId, call:items.map | dep: react, react-router-dom, lucide-react, @/components/ui/alert, @/components/ui/button, @/components/ui/skeleton, ../hooks/useDashboards, ../hooks/useWidgets, ../components/PinnedServiceLink, ../components/WidgetInstance, ../components/WidgetConfigDialog, useDashboards hook, useWidgets hook, PinnedServiceLink, WidgetInstanceCard, WidgetConfigDialog -- ServicePage.tsx | Displays a detailed service instance page with tabbed content, instance switching, and widget information based on URL parameters. | exp: func:ServicePage(), call:useParams, call:useServiceInstances, call:useNavigate, call:useMemo, call:services.find, call:getServiceBinding, call:serviceContentTabs, call:services.filter, call:binding.widgets.map, call:siblings.map, call:navigate, call:contentTabs.map, call:allTabs.map | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/tabs, ../hooks/useServices, ../types, ../components/SectionCard, ../integrations/registry, ./service-tabs +- ServicePage.tsx | Displays a detailed dashboard page for a specific service instance with overview, content tabs, and widgets. | exp: func:ServicePage(), call:useParams, call:useServiceInstances, call:useNavigate, call:useMemo, call:services.find, call:getServiceBinding, call:serviceContentTabs, call:services.filter, call:binding.widgets.map, call:navigate, call:encodeURIComponent, call:siblings.map, call:contentTabs.map, call:allTabs.map | dep: react, react-router-dom, lucide-react, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/tabs, ../hooks/useServices, ../types, ../components/SectionCard, ../integrations/registry, ./service-tabs, @/components/ui, @/components/SectionCard - ServiceTypePage.tsx | Resolves the first enabled service instance for a given type and redirects to it, or shows an empty state if none are configured. | exp: func:ServiceTypePage(), call:useParams, call:useServiceInstances, call:useMemo, call:instances.find | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/button, ../hooks/useServices, useServices hook -- ServicesPage.tsx | Provides the UI and logic for configuring external service instances and managing named dashboards with pinned links. | exp: func:ServicesPage(), call:useNavigate, call:useServiceInstances, call:useServiceTypes, call:useDeleteServiceInstance, call:useState, call:useMemo, call:map.get, call:list.push, call:map.set, call:[...map.entries()].sort, call:map.entries, call:a[0].localeCompare, call:types.find, call:getServiceBinding, call:setCreateOpen, call:grouped.map, call:typeName, call:instances.map, call:Object.entries(s.secrets_set).some, call:navigate, call:setDeleteId, call:Boolean, call:deleteService.mutate | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/input, @/components/ui/label, @/components/ui/switch, @/components/ui/dialog, @/components/ui/select, lucide-react, ../hooks/useServices, ../hooks/useDashboards, ../types, ../components/SectionCard, ../components/ConfirmDialog, ../components/DialogFooter, ../integrations/registry, ../components/PinnedServiceLink, ../api/dashboards, @/components/ui/*, useServices, useDashboards, SectionCard, ConfirmDialog, DialogFooter, integrations/registry -- Settings.tsx | Provides the settings page UI for managing monitoring machines, SSH keys, services, and database resets, including CRUD operations and SSH connection validation. | exp: func:Settings(), call:useMonitoringSettings, call:useSSHKeys, call:useSaveMonitoringMachine, call:useDeleteMonitoringMachine, call:useTestMonitoringMachineSSH, call:useState, call:emptyMachine, call:useIsMobile, call:useMemo, call:orderedMachines.find, call:setSSHValidationMessage, call:setSSHValidationError, call:setSSHValidationStatus, call:clearSSHValidation, call:setMachineDraft, call:setEditingMachine, call:setMachineDialogOpen, call:saveMachine.mutateAsync, call:testMachineSSH.mutateAsync, call:String, call:message.toLowerCase, call:lowered.includes, call:setTab, call:orderedMachines.map, call:setSelectedMachineId, call:cn, call:openEditMachine, call:setDeleteMachineId, call:closeMachineDialog, call:saveMachineDraft, call:machineDraft.host.trim, call:isMachineDraftDirty, call:Boolean, call:deleteMachine.mutate | dep: react, ../types, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../components/DialogFooter, ../components/HoverEditButton, ../components/SectionCard, ../components/SelectionRailCard, ../components/TabbedCard, ../components/ConfirmDialog, @/lib/utils, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/checkbox, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/switch, @/components/ui/tabs, @/components/ui/textarea, ../hooks/useServices, @/components/ui/*, @/hooks/useSettings, @/hooks/useServices, @/hooks/useIsMobile +- ServicesPage.tsx | Renders the services management page, allowing users to create, configure, test, and delete service instances, as well as manage custom dashboards composed of pinned service links. | exp: func:ServicesPage(), call:useNavigate, call:useServiceInstances, call:useServiceTypes, call:useDeleteServiceInstance, call:useState, call:useMemo, call:map.get, call:list.push, call:map.set, call:[...map.entries()].sort, call:map.entries, call:a[0].localeCompare, call:types.find, call:getServiceBinding, call:setCreateOpen, call:grouped.map, call:typeName, call:instances.map, call:Object.entries(s.secrets_set).some, call:navigate, call:setDeleteId, call:Boolean, call:deleteService.mutate | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/input, @/components/ui/label, @/components/ui/textarea, @/components/ui/switch, @/components/ui/dialog, @/components/ui/select, lucide-react, ../hooks/useServices, ../hooks/useDashboards, ../types, ../components/SectionCard, ../components/ConfirmDialog, ../components/DialogFooter, ../integrations/registry, ../components/ServiceTestPanel, ../components/PinnedServiceLink, ../api/dashboards, @/components/ui/*, useServices, useDashboards, SectionCard, ConfirmDialog, DialogFooter, ServiceTestPanel, getServiceBinding, serviceLinkTarget +- Settings.tsx | Provides a settings dashboard UI for managing monitoring machines, SSH keys, service instances, and danger zone operations with CRUD forms and validation. | exp: func:Settings(), call:useMonitoringSettings, call:useSSHKeys, call:useSaveMonitoringMachine, call:useDeleteMonitoringMachine, call:useTestMonitoringMachineSSH, call:useSearchParams, call:useState, call:searchParams.get, call:emptyMachine, call:useIsMobile, call:useMemo, call:orderedMachines.find, call:setSSHValidationMessage, call:setSSHValidationError, call:setSSHValidationStatus, call:clearSSHValidation, call:setMachineDraft, call:setEditingMachine, call:setMachineDialogOpen, call:saveMachine.mutateAsync, call:testMachineSSH.mutateAsync, call:String, call:message.toLowerCase, call:lowered.includes, call:setTab, call:orderedMachines.map, call:setSelectedMachineId, call:cn, call:openEditMachine, call:setDeleteMachineId, call:closeMachineDialog, call:saveMachineDraft, call:machineDraft.host.trim, call:isMachineDraftDirty, call:Boolean, call:deleteMachine.mutate | dep: react, react-router-dom, ../types, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../components/DialogFooter, ../components/HoverEditButton, ../components/SectionCard, ../components/SelectionRailCard, ../components/TabbedCard, ../components/ConfirmDialog, @/lib/utils, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/checkbox, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/switch, @/components/ui/tabs, @/components/ui/textarea, ../hooks/useServices, ../components/ServiceTestPanel, ../components (DialogFooter, SectionCard, SelectionRailCard, TabbedCard, ConfirmDialog, HoverEditButton, ServiceTestPanel), @/components/ui (multiple UI primitives) ## arch -React functional components using URL-parameter-driven routing, tabbed interfaces, and dialog-based CRUD management with widget composition. +Route-level React page components using composition of widgets, dialogs, and CRUD forms with slug/ID-based data resolution and conditional rendering for empty states. ## tags -call:use, components, ui, call:set, service, machine, page, dashboard +call:use, components, ui, call:set, service, machine, dashboard, page ## symbols - Dashboard - NamedDashboardPage diff --git a/frontend/src/pages/ServicePage.tsx b/frontend/src/pages/ServicePage.tsx index 9419f97..09b9106 100644 --- a/frontend/src/pages/ServicePage.tsx +++ b/frontend/src/pages/ServicePage.tsx @@ -1,7 +1,9 @@ import { useMemo } from "react"; import { useParams, useNavigate } from "react-router-dom"; +import { Settings as SettingsIcon } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useServiceInstances } from "../hooks/useServices"; import type { ServiceInstance } from "../types"; @@ -89,7 +91,21 @@ export function ServicePage() {

{instance.name}

{binding.description}

- {binding.name} +
+ {binding.name} + +
{/* Instance tabs (only when >1 enabled sibling) */} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index ef86231..5faedf1 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from "react"; import { useMemo, useState } from "react"; +import { useSearchParams } from "react-router-dom"; import type { MonitoringMachine, MonitoringMachineInput, @@ -893,7 +894,11 @@ export function Settings() { const saveMachine = useSaveMonitoringMachine(); const deleteMachine = useDeleteMonitoringMachine(); const testMachineSSH = useTestMonitoringMachineSSH(); - const [tab, setTab] = useState("machines"); + const [searchParams] = useSearchParams(); + const [tab, setTab] = useState( + (searchParams.get("tab") as SettingsTab | null) ?? "machines", + ); + const initialServiceId = searchParams.get("service") ?? ""; const [deleteMachineId, setDeleteMachineId] = useState(null); const [selectedSSHKeyId, setSelectedSSHKeyId] = useState(""); const [sshValidationMessage, setSSHValidationMessage] = useState(""); @@ -1195,7 +1200,9 @@ export function Settings() { onSelectKeyId={setSelectedSSHKeyId} /> )} - {tab === "services" && } + {tab === "services" && ( + + )} {tab === "danger" && } {isMobile ? ( @@ -1338,10 +1345,14 @@ export function Settings() { * old ServicePage ConfigBody — the service page is now a pure operational * view; all administration lives here. */ -function ServicesAdminCard() { +function ServicesAdminCard({ + initialServiceId = "", +}: { + initialServiceId?: string; +}) { const { data: services = [] } = useServiceInstances(); const { data: types = [] } = useServiceTypes(); - const [selectedServiceId, setSelectedServiceId] = useState(""); + const [selectedServiceId, setSelectedServiceId] = useState(initialServiceId); const sortedServices = useMemo( () => @@ -1416,6 +1427,11 @@ function ServicesAdminCard() { > {selectedService ? ( diff --git a/frontend/src/pages/__tests__/.pi-map.index.md b/frontend/src/pages/__tests__/.pi-map.index.md index 770b5c8..659e484 100644 --- a/frontend/src/pages/__tests__/.pi-map.index.md +++ b/frontend/src/pages/__tests__/.pi-map.index.md @@ -2,7 +2,7 @@ dir: frontend/src/pages/__tests__ ## role -Test suite for top-level page components, ensuring correct rendering, user interactions, and state handling across Dashboard, NamedDashboard, Service, and Settings views. +Test suite covering dashboard, service, and settings page components with mocked hooks and React Testing Library. ## parent index: frontend/src/pages/.pi-map.index.md map: frontend/src/pages/.pi-map.md @@ -12,6 +12,7 @@ map: frontend/src/pages/.pi-map.md - Dashboard.test.tsx - NamedDashboardPage.test.tsx - ServicePage.test.tsx +- Settings.services.test.tsx - Settings.test.tsx ## links index: frontend/src/pages/__tests__/.pi-map.index.md diff --git a/frontend/src/pages/__tests__/.pi-map.md b/frontend/src/pages/__tests__/.pi-map.md index 4039b29..bffa699 100644 --- a/frontend/src/pages/__tests__/.pi-map.md +++ b/frontend/src/pages/__tests__/.pi-map.md @@ -4,16 +4,17 @@ dir: frontend/src/pages/__tests__ index: frontend/src/pages/__tests__/.pi-map.index.md ## role -Test suite for top-level page components, ensuring correct rendering, user interactions, and state handling across Dashboard, NamedDashboard, Service, and Settings views. +Test suite covering dashboard, service, and settings page components with mocked hooks and React Testing Library. ## files - Dashboard.test.tsx | Tests the Dashboard component's shortcut CRUD functionality (creation, deletion, empty state rendering) while stubbing out composed widgets and data hooks. | dep: vitest, @testing-library/react, @testing-library/user-event, ../Dashboard, ../../types, react-router-dom, Dashboard, DashboardShortcut - NamedDashboardPage.test.tsx | Tests the NamedDashboardPage component covering loading, error/404, data rendering, and empty states using mocked hooks. | dep: vitest, @testing-library/react, react-router-dom, @tanstack/react-query, ../NamedDashboardPage, ../../hooks/useDashboards, NamedDashboardPage, useDashboards, useWidgets, WidgetConfigDialog, WidgetInstance - ServicePage.test.tsx | Tests the ServicePage component's tab rendering logic, verifying conditional display of tabs based on service type and instance count. | dep: vitest, @testing-library/react, @testing-library/user-event, react-router-dom, ../ServicePage, ../../types, ServicePage, ServiceInstance -- Settings.test.tsx | Tests the Settings component's machine list rendering, editing, and deletion flows with mocked hooks. | dep: vitest, @testing-library/react, @testing-library/user-event, ../Settings, ../../types, Settings, MonitoringMachine type, useSettings hooks +- Settings.services.test.tsx | Regression test verifying that the Settings component correctly resets editor state when switching services to prevent saving stale data from a previously selected service. | dep: vitest, @testing-library/react, @testing-library/user-event, react-router-dom, ../Settings, ../../hooks/useSettings, ../../hooks/useServices +- Settings.test.tsx | Tests the Settings component's machine CRUD operations (render, edit/save, delete) using mocked hooks and React Testing Library. | dep: vitest, @testing-library/react, @testing-library/user-event, react-router-dom, ../Settings, ../../types, Settings, MonitoringMachine ## arch -React Testing Library with Jest, using mocked hooks and stubbed child components to isolate page-level rendering logic and CRUD workflows. +Standard React Testing Library pattern using mocked hooks/data sources, component rendering verification, and user interaction simulation across isolated test files per page component. ## tags -react, @testing, library, dashboard, tests, component, rendering, hooks +react, library, @testing, hooks, dashboard, component, vitest, router ## symbols - ## workflows diff --git a/frontend/src/pages/__tests__/Settings.services.test.tsx b/frontend/src/pages/__tests__/Settings.services.test.tsx new file mode 100644 index 0000000..ffa3676 --- /dev/null +++ b/frontend/src/pages/__tests__/Settings.services.test.tsx @@ -0,0 +1,143 @@ +/** + * Regression test for the service-editor state-reset bug. + * + * ServiceConfigEditor initializes its editable state (name, config, secrets) + * from the `instance` prop via useState. The parent must remount it (via + * `key={instance.id}`) when the selected service changes — otherwise switching + * services in the rail keeps the PREVIOUS service's name/config in state while + * `instance.id`/`service_type` (read live from props) now point at the new one. + * A save then wrote the stale name/config onto the new service's row + * (e.g. saving qBittorrent renamed it "Jellyfin" with Jellyfin's URL). + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; +import { Settings } from "../Settings"; + +const saveServiceMutate = vi.fn().mockResolvedValue({}); + +// Settings invokes the machine/SSH hooks at the top level regardless of active +// tab, so stub them out to keep the render focused on the Services editor. +vi.mock("../../hooks/useSettings", () => ({ + useMonitoringSettings: () => ({ data: [] }), + useSSHKeys: () => ({ data: [] }), + useSaveMonitoringMachine: () => ({ mutateAsync: vi.fn(), isPending: false }), + useDeleteMonitoringMachine: () => ({ mutate: vi.fn() }), + useTestMonitoringMachineSSH: () => ({ + mutateAsync: vi.fn(), + isPending: false, + }), + useResetLocalDatabase: () => ({}), + useSaveSSHKey: () => ({ mutateAsync: vi.fn() }), + useGenerateSSHKey: () => ({ mutateAsync: vi.fn(), isPending: false }), + useDeleteSSHKey: () => ({ mutate: vi.fn() }), +})); + +vi.mock("../../hooks/useServices", () => ({ + useServiceTypes: () => ({ + data: [ + { + service_type: "jellyfin", + name: "Jellyfin", + description: "Media server", + config_schema: { + type: "object", + properties: { + base_url: { type: "string", description: "URL" }, + timeout_seconds: { type: "integer" }, + }, + }, + secret_fields: [{ key: "api_key", label: "API key", required: true }], + widget_kinds: [], + }, + { + service_type: "qbittorrent", + name: "qBittorrent", + description: "Torrent client", + config_schema: { + type: "object", + properties: { + base_url: { type: "string" }, + timeout_seconds: { type: "integer" }, + }, + }, + secret_fields: [ + { key: "username", label: "Username", required: true }, + { key: "password", label: "Password", required: true }, + ], + widget_kinds: [], + }, + ], + }), + useServiceInstances: () => ({ + data: [ + { + id: "svc-jf", + service_type: "jellyfin", + name: "My Jellyfin", + config: { base_url: "https://jf.example.com", timeout_seconds: 60 }, + secrets_set: { api_key: true }, + enabled: true, + created_at: 0, + updated_at: 0, + }, + { + id: "svc-qb", + service_type: "qbittorrent", + name: "My qBittorrent", + config: { base_url: "https://qb.example.com", timeout_seconds: 60 }, + secrets_set: { username: true, password: true }, + enabled: true, + created_at: 0, + updated_at: 0, + }, + ], + }), + useSaveServiceInstance: () => ({ + mutateAsync: saveServiceMutate, + isPending: false, + }), + useDeleteServiceInstance: () => ({ mutate: vi.fn() }), + useTestServiceInstance: () => ({ + mutateAsync: vi.fn().mockResolvedValue({ ok: true }), + isPending: false, + }), +})); + +beforeEach(() => { + saveServiceMutate.mockClear(); +}); + +describe("Settings > Services editor", () => { + it("resets editable state when switching services so save does not clobber (regression)", async () => { + render( + + + , + ); + + // Default selection is the first service (Jellyfin). Switch to qBittorrent + // via the selection rail. + await userEvent.click(screen.getByText("My qBittorrent")); + + // Save is gated on a passed test or "Save anyway"; toggle Save anyway so + // we can save without exercising the (mocked) test mutation. + await userEvent.click(screen.getByLabelText(/save anyway/i)); + + await userEvent.click(screen.getByRole("button", { name: "Save" })); + + expect(saveServiceMutate).toHaveBeenCalledTimes(1); + const saved = saveServiceMutate.mock.calls[0][0]; + // Must target qBittorrent's id AND carry qBittorrent's own name/config — + // not the stale Jellyfin values from the previously selected service. + expect(saved.id).toBe("svc-qb"); + expect(saved.service_type).toBe("qbittorrent"); + expect(saved.name).toBe("My qBittorrent"); + expect(saved.config).toEqual({ + base_url: "https://qb.example.com", + timeout_seconds: 60, + }); + }); +}); diff --git a/frontend/src/pages/__tests__/Settings.test.tsx b/frontend/src/pages/__tests__/Settings.test.tsx index debb550..6652878 100644 --- a/frontend/src/pages/__tests__/Settings.test.tsx +++ b/frontend/src/pages/__tests__/Settings.test.tsx @@ -1,17 +1,16 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; import { Settings } from "../Settings"; import type { MonitoringMachine } from "../../types"; const saveMachineMutate = vi.fn().mockResolvedValue({}); const deleteMachineMutate = vi.fn(); -const testSSHMutate = vi - .fn() - .mockResolvedValue({ - message: "SSH auth succeeded", - known_hosts_updated: true, - }); +const testSSHMutate = vi.fn().mockResolvedValue({ + message: "SSH auth succeeded", + known_hosts_updated: true, +}); let machines: MonitoringMachine[] = []; @@ -66,14 +65,21 @@ beforeEach(() => { describe("Settings", () => { it("renders the machine list from the mocked store", () => { machines = [localMachine()]; - render(); - // The rail row caption (mode · enabled) is unique to the selection rail. + render( + + + , + ); expect(screen.getByText("local · Enabled")).toBeInTheDocument(); }); it("saves a machine via the editor dialog (controlled useState parity)", async () => { machines = [localMachine()]; - render(); + render( + + + , + ); // The detail-pane "Edit" has visible text "Edit"; the rail hover edit // affordance is icon-only (aria-label "Edit") — disambiguate by text. @@ -99,7 +105,11 @@ describe("Settings", () => { it("deletes a machine through the confirm dialog", async () => { machines = [localMachine()]; - render(); + render( + + + , + ); // Detail-pane "Delete" opens the confirm dialog. await userEvent.click(screen.getByRole("button", { name: "Delete" }));