Skip to content
Source
Sign inSign up

device.browser.test.tsx

185 lines6.3 KB

1import { RegistryContext } from "@effect/atom-react";
2import {
3 EnvironmentAuthorizationError,
4 EnvironmentId,
5 ThreadId,
6 type DeviceServiceState,
7} from "@t3tools/contracts";
8import { scopedThreadKey } from "@t3tools/client-runtime/environment";
9import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
10import { expect, it, vi } from "vite-plus/test";
11import { page } from "vite-plus/test/browser";
12import { useEffect } from "react";
13​
14import { act, render } from "../../test/render";
15import { DevicePanel } from "../components/device/DevicePanel";
16import {
17 startAgentOpenedPreviews,
18 resetDeviceSessionBaselinesForTests,
19} from "../components/preview/agentOpenedPreviews";
20import { usePreviewMiniPlayerStore } from "../previewMiniPlayerStore";
21import { appAtomRegistry } from "../rpc/atomRegistry";
22import { deviceEnvironment, useDeviceState } from "./device";
23​
24vi.mock(import("./presentation"), async (original) => ({
25 ...(await original()),
26 environmentSummaries: {
27 ...(await original()).environmentSummaries,
28 connectedEnvironmentIdsAtom: Atom.make([EnvironmentId.make("device-loading")]),
29 },
30}));
31vi.mock(import("../hooks/useSettings"), () => ({
32 getClientSettings: (() => ({ browserAutoShowFloatingPreview: true })) as never,
33 useClientSettings: ((
34 select: (settings: { browserAutoShowFloatingPreview: boolean }) => unknown,
35 ) => select({ browserAutoShowFloatingPreview: true })) as never,
36}));
37vi.mock(import("./use-atom-command"), () => {
38 const run = () => Promise.resolve({ _tag: "Success", value: {} });
39 return { useAtomCommand: (() => run) as never };
40});
41vi.mock(import("../components/device/DeviceHostUpdates"), () => ({
42 DeviceHostUpdates: () => null,
43}));
44​
45it("baselines the first device snapshot without reopening dismissed devices", async () => {
46 const viewport = { width: window.innerWidth, height: window.innerHeight };
47 await page.viewport(1280, 800);
48 await using _viewport = {
49 [Symbol.asyncDispose]: () => page.viewport(viewport.width, viewport.height),
50 };
51 const threadRef = {
52 environmentId: EnvironmentId.make("device-loading"),
53 threadId: ThreadId.make("thread"),
54 };
55 const threadKey = scopedThreadKey(threadRef);
56 const snapshot: DeviceServiceState = {
57 hosts: [],
58 hostStatus: "ready",
59 hostStatuses: {},
60 devices: [
61 {
62 hostId: "local",
63 id: "phone",
64 platform: "ios",
65 name: "Phone",
66 version: "26",
67 booted: true,
68 physical: false,
69 },
70 ],
71 sessions: [
72 {
73 threadId: threadRef.threadId,
74 hostId: "local",
75 deviceId: "phone",
76 platform: "ios",
77 openedAt: "2026-10-01T00:00:00Z",
78 },
79 ],
80 onboardingCompleted: true,
81 agentAccessEnabled: true,
82 hubBasePath: "/api/device-hub",
83 revision: 1,
84 };
85 const registry = appAtomRegistry;
86 resetDeviceSessionBaselinesForTests();
87 using _baselines = { [Symbol.dispose]: resetDeviceSessionBaselinesForTests };
88 const stateAtom = Atom.make<AsyncResult.AsyncResult<DeviceServiceState>>(AsyncResult.initial());
89 using _subscription = vi.spyOn(deviceEnvironment, "state").mockReturnValue(stateAtom);
90 using _store = {
91 [Symbol.dispose]: () => usePreviewMiniPlayerStore.getState().removeThread(threadRef),
92 };
93 using _following = { [Symbol.dispose]: startAgentOpenedPreviews() };
94 let loaded = false;
95 function Probe() {
96 const device = useDeviceState(threadRef.environmentId);
97 useEffect(() => {
98 loaded = device.loaded;
99 }, [device.loaded]);
100 return null;
101 }
102 await using _screen = await render(
103 <RegistryContext.Provider value={registry}>
104 <Probe />
105 </RegistryContext.Provider>,
106 );
107 const initiallyLoaded = loaded;
108 await act(() => registry.set(stateAtom, AsyncResult.success(snapshot)));
109 expect(usePreviewMiniPlayerStore.getState().byThreadKey[threadKey]).toBeUndefined();
110 expect(initiallyLoaded).toBe(false);
111 expect(loaded).toBe(true);
112​
113 await act(() => registry.set(stateAtom, AsyncResult.waiting(AsyncResult.success(snapshot))));
114 expect(loaded).toBe(true);
115​
116 const nextDevice = { ...snapshot.devices[0]!, id: "tablet", name: "Tablet" };
117 await act(() =>
118 registry.set(
119 stateAtom,
120 AsyncResult.success({
121 ...snapshot,
122 devices: [...snapshot.devices, nextDevice],
123 sessions: [...snapshot.sessions, { ...snapshot.sessions[0]!, deviceId: nextDevice.id }],
124 revision: 2,
125 }),
126 ),
127 );
128 expect(usePreviewMiniPlayerStore.getState().byThreadKey[threadKey]?.source).toEqual({
129 kind: "device",
130 hostId: "local",
131 deviceId: "tablet",
132 platform: "ios",
133 name: "Tablet",
134 });
135});
136​
137it("reports a failed first device load and retries it", async () => {
138 let attempts = 0;
139 const stateAtom = Atom.make(
140 (): AsyncResult.AsyncResult<DeviceServiceState, EnvironmentAuthorizationError> => {
141 attempts += 1;
142 return attempts === 1
143 ? AsyncResult.fail(
144 new EnvironmentAuthorizationError({
145 requiredScope: "orchestration:read",
146 message: "Device access denied",
147 }),
148 )
149 : AsyncResult.success({
150 hosts: [],
151 hostStatus: "ready",
152 hostStatuses: {},
153 devices: [],
154 sessions: [],
155 onboardingCompleted: true,
156 agentAccessEnabled: false,
157 hubBasePath: "/api/device-hub",
158 revision: 1,
159 });
160 },
161 ).pipe(Atom.keepAlive);
162 const registry = AtomRegistry.make();
163 using _registry = { [Symbol.dispose]: () => registry.dispose() };
164 using _subscription = vi.spyOn(deviceEnvironment, "state").mockReturnValue(stateAtom);
165 await using screen = await render(
166 <RegistryContext.Provider value={registry}>
167 <DevicePanel
168 mode="embedded"
169 threadRef={{
170 environmentId: EnvironmentId.make("device-load-failure"),
171 threadId: ThreadId.make("thread"),
172 }}
173 surface={{ id: "device", kind: "device" }}
174 visible
175 onDismissSetup={() => undefined}
176 />
177 </RegistryContext.Provider>,
178 );
179​
180 await expect.element(screen.getByRole("alert").getByText("Device access denied")).toBeVisible();
181​
182 await screen.getByRole("button", { name: "Retry" }).click();
183 await expect.element(screen.getByRole("button", { name: "Refresh devices" })).toBeVisible();
184 expect(screen.container.querySelector('[role="alert"]')).toBeNull();
185});