1import {
2 DEFAULT_CLIENT_SETTINGS,
3 DEFAULT_UNIFIED_SETTINGS,
4 type DeviceServiceState,
5} from "@t3tools/contracts";
6import {
7 createMemoryHistory,
8 createRootRoute,
9 createRouter,
10 RouterProvider,
11} from "@tanstack/react-router";
12import { act, StrictMode, type ReactNode } from "react";
13import { create, type ReactTestRenderer } from "react-test-renderer";
14import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
15
16const { listBrowserImportSources } = vi.hoisted(() => ({
17 listBrowserImportSources: vi.fn().mockResolvedValue([]),
18}));
19
20vi.mock("../ui/tooltip", () => ({
21 Tooltip: ({ children }: { children: ReactNode }) => children,
22 TooltipTrigger: ({ render, children }: { render: ReactNode; children: ReactNode }) => (
23 <>
24 {render}
25 {children}
26 </>
27 ),
28 TooltipPopup: () => null,
29}));
30vi.mock("../preview/previewBridge", () => ({
31 previewBridge: { listBrowserImportSources },
32}));
33vi.mock("../../env", () => ({ isElectron: true }));
34vi.mock("../../state/environments", () => ({
35 useEnvironments: () => ({ environments: [], isReady: true }),
36 usePrimaryEnvironment: () => null,
37 // Settings rows resolve the primary grant before rendering server controls.
38 usePrimaryEnvironmentId: () => null,
39}));
40vi.mock("../../hooks/useSettings", async (importOriginal) => ({
41 ...(await importOriginal<typeof import("../../hooks/useSettings")>()),
42 PRIMARY_SETTINGS_UNAVAILABLE_MESSAGE: "Connect to an environment",
43 useClientSettings: (selector?: (settings: typeof DEFAULT_CLIENT_SETTINGS) => unknown) =>
44 selector ? selector(DEFAULT_CLIENT_SETTINGS) : DEFAULT_CLIENT_SETTINGS,
45 useClientSettingsHydrated: () => true,
46 usePrimarySettingsAvailable: () => true,
47 usePrimarySettings: () => DEFAULT_UNIFIED_SETTINGS,
48 useUpdatePrimarySettings: () => vi.fn(),
49}));
50vi.mock("./settingsLayout", async (importOriginal) => ({
51 ...(await importOriginal<typeof import("./settingsLayout")>()),
52 SettingsPageContainer: ({ children }: { children: ReactNode }) => children,
53}));
54// The scoped agent-access rows need the settings layout's scope provider;
55// this test covers the device-local browser sections only.
56vi.mock("./ProjectDefaultsSettings", () => ({ ProjectDefaultsSettings: () => null }));
57vi.mock("./SettingsScopeContext", () => ({
58 useSettingsScope: () => ({
59 scope: { kind: "all", environmentIds: [] },
60 search: {},
61 environment: null,
62 environments: [],
63 target: null,
64 connectedEnvironments: [],
65 targets: [],
66 }),
67 useOptionalSettingsScope: () => null,
68}));
69
70import { IntegrationsSettingsPanel } from "./IntegrationsSettings";
71import { platformSetupStatus } from "../device/DeviceSetup";
72
73let renderer: ReactTestRenderer | undefined;
74
75beforeEach(() => {
76 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
77 listBrowserImportSources.mockClear();
78});
79
80afterEach(async () => {
81 await act(() => renderer?.unmount());
82 vi.unstubAllGlobals();
83});
84
85async function openSettings() {
86 const router = createRouter({
87 routeTree: createRootRoute({ component: IntegrationsSettingsPanel }),
88 history: createMemoryHistory(),
89 });
90 await router.load();
91 await act(() => {
92 renderer = create(
93 <StrictMode>
94 <RouterProvider router={router} />
95 </StrictMode>,
96 );
97 });
98 expect(renderer!.root.findByType(IntegrationsSettingsPanel)).toBeDefined();
99}
100
101describe("Integrations browser discovery", () => {
102 it("does not scan browser files when entering or revisiting settings", async () => {
103 await openSettings();
104 expect(listBrowserImportSources).not.toHaveBeenCalled();
105
106 await act(() => renderer?.unmount());
107 await openSettings();
108 expect(listBrowserImportSources).not.toHaveBeenCalled();
109 });
110
111 it("places device settings directly after browser settings", async () => {
112 await openSettings();
113 const sections = renderer!.root
114 .findAll((node) => node.type === "section")
115 .map((node) => node.props.id)
116 .filter(Boolean);
117 expect(sections.indexOf("devices")).toBeGreaterThan(sections.indexOf("browser"));
118 });
119});
120
121const deviceState = (overrides: Partial<DeviceServiceState> = {}): DeviceServiceState => ({
122 hosts: [
123 {
124 id: "local",
125 kind: "local",
126 label: "This machine",
127 hubInstalled: false,
128 agentDeviceInstalled: false,
129 platforms: [
130 { platform: "ios", available: true },
131 { platform: "android", available: true },
132 ],
133 },
134 ],
135 hostStatus: "ready",
136 hostStatuses: {},
137 devices: [],
138 sessions: [],
139 onboardingCompleted: false,
140 agentAccessEnabled: false,
141 hubBasePath: "/api/device-hub",
142 revision: 0,
143 ...overrides,
144});
145
146describe("device setup guidance", () => {
147 it("directs users to install an iOS runtime and create an Android virtual device", () => {
148 expect(platformSetupStatus(deviceState(), "ios").message).toContain("Xcode Settings");
149 expect(platformSetupStatus(deviceState(), "android").message).toContain("Device Manager");
150 });
151
152 it("preserves a specific missing-tool explanation from the server", () => {
153 const state = deviceState({
154 hosts: [
155 {
156 ...deviceState().hosts[0]!,
157 platforms: [
158 { platform: "ios", available: true },
159 { platform: "android", available: false, reason: "Android Emulator is missing." },
160 ],
161 },
162 ],
163 });
164 expect(platformSetupStatus(state, "android").message).toBe("Android Emulator is missing.");
165 });
166});