| 1 | import { RegistryContext } from "@effect/atom-react"; |
| 2 | import { |
| 3 | BearerConnectionTarget, |
| 4 | PrimaryConnectionTarget, |
| 5 | } from "@t3tools/client-runtime/connection"; |
| 6 | import { |
| 7 | AuthTerminalOperateScope, |
| 8 | DEFAULT_SERVER_SETTINGS, |
| 9 | EnvironmentId, |
| 10 | ProviderDriverKind, |
| 11 | ProviderInstanceId, |
| 12 | type AuthEnvironmentScope, |
| 13 | type AuthSessionState, |
| 14 | type ResolvedKeybindingsConfig, |
| 15 | type ServerProvider, |
| 16 | type ServerSettings, |
| 17 | type TerminalCloseInput, |
| 18 | type TerminalOpenInput, |
| 19 | type TerminalWriteInput, |
| 20 | } from "@t3tools/contracts"; |
| 21 | import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings"; |
| 22 | import * as Cause from "effect/Cause"; |
| 23 | import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity"; |
| 24 | import { act, create, type ReactTestInstance, type ReactTestRenderer } from "react-test-renderer"; |
| 25 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 26 | |
| 27 | type SessionResult = AsyncResult.AsyncResult<AuthSessionState, Error>; |
| 28 | type CommandResult = AsyncResult.AsyncResult<void, Error>; |
| 29 | type Request<Input> = { environmentId: EnvironmentId; input: Input }; |
| 30 | type SetupConfig = { |
| 31 | cwd: string; |
| 32 | settings: ServerSettings; |
| 33 | environment: { platform: { os: "darwin" } }; |
| 34 | keybindings: ResolvedKeybindingsConfig; |
| 35 | }; |
| 36 | const state = vi.hoisted(() => ({ |
| 37 | registry: null as AtomRegistry.AtomRegistry | null, |
| 38 | sessions: new Map<EnvironmentId, Atom.Writable<SessionResult>>(), |
| 39 | providers: new Map<EnvironmentId, Atom.Writable<ReadonlyArray<ServerProvider>>>(), |
| 40 | configs: new Map<EnvironmentId, Atom.Atom<SetupConfig>>(), |
| 41 | open: vi.fn<(request: Request<TerminalOpenInput>) => Promise<CommandResult>>(), |
| 42 | write: vi.fn<(request: Request<TerminalWriteInput>) => Promise<CommandResult>>(), |
| 43 | close: vi.fn<(request: Request<TerminalCloseInput>) => Promise<CommandResult>>(), |
| 44 | refresh: vi.fn(), |
| 45 | pair: vi.fn(), |
| 46 | createProject: vi.fn(), |
| 47 | importThreads: vi.fn(), |
| 48 | complete: vi.fn<() => Promise<void>>(), |
| 49 | done: vi.fn(), |
| 50 | })); |
| 51 | |
| 52 | vi.mock("../settings/SelfHostedServer", () => ({ RemoteConnectionFlow: () => null })); |
| 53 | vi.mock("../../connection/runtime", () => ({ connectionAtomRuntime: undefined })); |
| 54 | vi.mock("@t3tools/client-runtime/state/session", () => ({ |
| 55 | createEnvironmentSessionAtoms: () => ({ |
| 56 | sessionStateAtom: (id: EnvironmentId) => state.sessions.get(id)!, |
| 57 | }), |
| 58 | })); |
| 59 | vi.mock("../../rpc/atomRegistry", () => ({ |
| 60 | get appAtomRegistry() { |
| 61 | return state.registry; |
| 62 | }, |
| 63 | })); |
| 64 | vi.mock("../../state/environments", () => ({ |
| 65 | useEnvironments: () => ({ environments: [remoteEnvironment] }), |
| 66 | usePrimaryEnvironment: () => primaryEnvironment, |
| 67 | })); |
| 68 | vi.mock("../../state/server", () => ({ |
| 69 | serverEnvironment: { |
| 70 | providersValueAtom: (id: EnvironmentId) => state.providers.get(id)!, |
| 71 | configValueAtom: (id: EnvironmentId) => state.configs.get(id)!, |
| 72 | refreshProviders: "refresh", |
| 73 | }, |
| 74 | })); |
| 75 | vi.mock("../../state/terminal", () => ({ |
| 76 | terminalEnvironment: { open: "open", write: "write", close: "close" }, |
| 77 | })); |
| 78 | vi.mock("../../state/use-atom-command", () => ({ |
| 79 | useAtomCommand: ( |
| 80 | command: "open" | "write" | "close" | "refresh" | "pair" | "createProject" | "importThreads", |
| 81 | ) => state[command], |
| 82 | })); |
| 83 | vi.mock("../../connection/onboarding", () => ({ connectPairing: "pair" })); |
| 84 | vi.mock("../../state/agentSessions", () => ({ |
| 85 | agentSessionImport: "importThreads", |
| 86 | agentSessionScan: vi.fn(), |
| 87 | })); |
| 88 | vi.mock("../../state/projects", () => ({ projectEnvironment: { create: "createProject" } })); |
| 89 | vi.mock("../../state/entities", () => ({ readProjects: () => [], useProjects: () => [] })); |
| 90 | vi.mock("../../onboarding/useProjectScans", () => ({ useProjectScans: () => [] })); |
| 91 | vi.mock("../ui/dialog", () => ({ |
| 92 | Dialog: "div", |
| 93 | DialogPopup: "div", |
| 94 | DialogTitle: "div", |
| 95 | DialogHeader: "div", |
| 96 | })); |
| 97 | vi.mock("../ui/wizard", () => ({ |
| 98 | WizardPanel: "div", |
| 99 | WizardPopup: "div", |
| 100 | WizardHeader: "div", |
| 101 | WizardSteps: ({ onStepChange }: { onStepChange: (step: number) => void }) => ( |
| 102 | <button onClick={() => onStepChange(0)}>Back</button> |
| 103 | ), |
| 104 | })); |
| 105 | vi.mock("../ui/scroll-area", () => ({ ScrollArea: "div" })); |
| 106 | vi.mock("../../onboarding/firstRun", () => ({ useCompleteOnboarding: () => state.complete })); |
| 107 | vi.mock("../../hooks/useTheme", () => ({ mountOnboardingTheme: () => undefined })); |
| 108 | vi.mock("../../hooks/useLocalStorage", () => ({ useLocalStorage: () => [false, vi.fn()] })); |
| 109 | vi.mock("../../hooks/useCopyToClipboard", () => ({ |
| 110 | useCopyToClipboard: () => ({ copyToClipboard: vi.fn(), isCopied: false }), |
| 111 | })); |
| 112 | vi.mock("../../cloud/publicConfig", () => ({ hasCloudPublicConfig: () => false })); |
| 113 | vi.mock("../clerk/useT3ConnectAuthPrompt", () => ({ useT3ConnectAuthPrompt: vi.fn() })); |
| 114 | vi.mock("@clerk/react", () => ({ useAuth: vi.fn() })); |
| 115 | vi.mock("../../env", () => ({ isElectron: false })); |
| 116 | vi.mock("../../providerInstances", () => ({ resolveDefaultProviderModelSelection: vi.fn() })); |
| 117 | vi.mock("../settings/ChatGptWelcomeCoordinator", () => ({ ChatGptWelcomeCoordinator: () => null })); |
| 118 | vi.mock("../settings/CodexSetupSection", () => ({ |
| 119 | CodexSetupSection: () => null, |
| 120 | AddManagedCodexAccountDialog: () => null, |
| 121 | })); |
| 122 | vi.mock("../settings/providerDriverMeta", () => ({ |
| 123 | providerClients: { get: (driver: string) => ({ label: driver }) }, |
| 124 | })); |
| 125 | vi.mock("../settings/providerStatus", () => ({ |
| 126 | getProviderSummary: () => ({ headline: "Setup required" }), |
| 127 | })); |
| 128 | vi.mock("../cloud/CloudEnvironmentConnectList", () => ({ |
| 129 | CloudEnvironmentConnectRows: () => null, |
| 130 | })); |
| 131 | vi.mock("../ThreadTerminalDrawer", () => ({ |
| 132 | TerminalViewport: () => <div data-terminal-viewport />, |
| 133 | })); |
| 134 | vi.mock("../ui/button", () => ({ Button: "button" })); |
| 135 | vi.mock("../ui/input", () => ({ Input: "input" })); |
| 136 | vi.mock("../ui/checkbox", () => ({ Checkbox: "input" })); |
| 137 | vi.mock("../ui/collapsible", () => ({ |
| 138 | Collapsible: "div", |
| 139 | CollapsiblePanel: "div", |
| 140 | CollapsibleTrigger: "button", |
| 141 | })); |
| 142 | vi.mock("../ui/toast", () => ({ |
| 143 | toastManager: { add: vi.fn(), update: vi.fn(), close: vi.fn() }, |
| 144 | })); |
| 145 | |
| 146 | import { WelcomeWizard } from "./WelcomeWizard"; |
| 147 | |
| 148 | const primaryId = EnvironmentId.make("primary"); |
| 149 | const remoteId = EnvironmentId.make("paired-remote"); |
| 150 | const primaryEnvironment = { |
| 151 | environmentId: primaryId, |
| 152 | label: "This computer", |
| 153 | connection: { phase: "connected" }, |
| 154 | entry: { |
| 155 | enabled: true, |
| 156 | target: new PrimaryConnectionTarget({ |
| 157 | environmentId: primaryId, |
| 158 | label: "This computer", |
| 159 | httpBaseUrl: "http://127.0.0.1:3773", |
| 160 | wsBaseUrl: "ws://127.0.0.1:3773", |
| 161 | }), |
| 162 | }, |
| 163 | } as const; |
| 164 | const remoteEnvironment = { |
| 165 | environmentId: remoteId, |
| 166 | label: "Paired computer", |
| 167 | connection: { phase: "connected" }, |
| 168 | entry: { |
| 169 | enabled: true, |
| 170 | target: new BearerConnectionTarget({ |
| 171 | environmentId: remoteId, |
| 172 | label: "Paired computer", |
| 173 | connectionId: "paired-remote", |
| 174 | }), |
| 175 | }, |
| 176 | } as const; |
| 177 | const missingClaude: ServerProvider = { |
| 178 | instanceId: ProviderInstanceId.make("claude-work"), |
| 179 | driver: ProviderDriverKind.make("claudeAgent"), |
| 180 | enabled: true, |
| 181 | installed: false, |
| 182 | version: null, |
| 183 | status: "error", |
| 184 | auth: { status: "unknown" }, |
| 185 | checkedAt: "2026-09-05T00:00:00.000Z", |
| 186 | models: [], |
| 187 | slashCommands: [], |
| 188 | skills: [], |
| 189 | }; |
| 190 | const signedOutCodex: ServerProvider = { |
| 191 | ...missingClaude, |
| 192 | instanceId: ProviderInstanceId.make("codex-work"), |
| 193 | driver: ProviderDriverKind.make("codex"), |
| 194 | installed: true, |
| 195 | auth: { status: "unauthenticated" }, |
| 196 | }; |
| 197 | const session = (scopes: readonly AuthEnvironmentScope[]): AuthSessionState => ({ |
| 198 | authenticated: true, |
| 199 | scopes, |
| 200 | auth: { |
| 201 | policy: "remote-reachable", |
| 202 | bootstrapMethods: ["one-time-token"], |
| 203 | sessionMethods: ["bearer-access-token"], |
| 204 | sessionCookieName: "t3_session", |
| 205 | }, |
| 206 | }); |
| 207 | function setAccess(environmentId: EnvironmentId, allowed: boolean) { |
| 208 | state.registry!.set( |
| 209 | state.sessions.get(environmentId)!, |
| 210 | AsyncResult.success(session(allowed ? [AuthTerminalOperateScope] : [])), |
| 211 | ); |
| 212 | } |
| 213 | function deferred<T>() { |
| 214 | let resolve!: (value: T) => void; |
| 215 | const promise = new Promise<T>((complete) => { |
| 216 | resolve = complete; |
| 217 | }); |
| 218 | return { promise, resolve }; |
| 219 | } |
| 220 | function text(node: ReactTestInstance | string): string { |
| 221 | return typeof node === "string" ? node : node.children.map(text).join(""); |
| 222 | } |
| 223 | let renderer: ReactTestRenderer | undefined; |
| 224 | function button(label: string) { |
| 225 | return renderer!.root.findAllByType("button").find((node) => text(node) === label)!; |
| 226 | } |
| 227 | async function click(label: string) { |
| 228 | const node = button(label); |
| 229 | expect(node).toBeDefined(); |
| 230 | expect(node.props.disabled).not.toBe(true); |
| 231 | await act(async () => node.props.onClick()); |
| 232 | } |
| 233 | function hasViewport() { |
| 234 | return renderer!.root.findAllByProps({ "data-terminal-viewport": true }).length > 0; |
| 235 | } |
| 236 | async function enterRemoteAgents() { |
| 237 | await act(async () => { |
| 238 | renderer = create( |
| 239 | <RegistryContext.Provider value={state.registry!}> |
| 240 | <WelcomeWizard localAvailable onDone={state.done} /> |
| 241 | </RegistryContext.Provider>, |
| 242 | ); |
| 243 | }); |
| 244 | await click("Continue"); |
| 245 | expect(text(renderer!.root)).toContain("Paired computer"); |
| 246 | } |
| 247 | |
| 248 | beforeEach(() => { |
| 249 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); |
| 250 | vi.stubGlobal("document", { activeElement: null, body: {}, getElementById: () => null }); |
| 251 | state.registry = AtomRegistry.make(); |
| 252 | for (const id of [primaryId, remoteId]) { |
| 253 | state.sessions.set( |
| 254 | id, |
| 255 | Atom.make<SessionResult>(AsyncResult.success(session([AuthTerminalOperateScope]))).pipe( |
| 256 | Atom.keepAlive, |
| 257 | ), |
| 258 | ); |
| 259 | state.providers.set( |
| 260 | id, |
| 261 | Atom.make<ReadonlyArray<ServerProvider>>([missingClaude, signedOutCodex]).pipe( |
| 262 | Atom.keepAlive, |
| 263 | ), |
| 264 | ); |
| 265 | state.configs.set( |
| 266 | id, |
| 267 | Atom.make<SetupConfig>({ |
| 268 | cwd: `/fixtures/${id}`, |
| 269 | settings: { |
| 270 | ...DEFAULT_SERVER_SETTINGS, |
| 271 | providerInstances: { |
| 272 | [signedOutCodex.instanceId]: { |
| 273 | driver: signedOutCodex.driver, |
| 274 | config: { binaryPath: "/opt/codex-work", setupMode: "existing" }, |
| 275 | }, |
| 276 | }, |
| 277 | }, |
| 278 | environment: { platform: { os: "darwin" } }, |
| 279 | keybindings: DEFAULT_RESOLVED_KEYBINDINGS, |
| 280 | }), |
| 281 | ); |
| 282 | } |
| 283 | state.open.mockReset().mockResolvedValue(AsyncResult.success(undefined)); |
| 284 | state.write.mockReset().mockResolvedValue(AsyncResult.success(undefined)); |
| 285 | state.close.mockReset().mockResolvedValue(AsyncResult.success(undefined)); |
| 286 | state.refresh.mockReset().mockResolvedValue(AsyncResult.success(undefined)); |
| 287 | state.pair.mockReset().mockResolvedValue(AsyncResult.success(remoteId)); |
| 288 | state.complete.mockReset().mockResolvedValue(undefined); |
| 289 | state.done.mockReset(); |
| 290 | }); |
| 291 | |
| 292 | afterEach(async () => { |
| 293 | await act(async () => renderer?.unmount()); |
| 294 | renderer = undefined; |
| 295 | state.registry?.dispose(); |
| 296 | state.sessions.clear(); |
| 297 | state.providers.clear(); |
| 298 | state.configs.clear(); |
| 299 | vi.unstubAllGlobals(); |
| 300 | }); |
| 301 | |
| 302 | describe("welcome agent terminal setup", () => { |
| 303 | it("disables both setup actions for the paired read-only connection and preserves local completion", async () => { |
| 304 | setAccess(remoteId, false); |
| 305 | await enterRemoteAgents(); |
| 306 | expect(button("Install").props.disabled).toBe(true); |
| 307 | expect(button("Sign in").props.disabled).toBe(true); |
| 308 | expect(text(renderer!.root)).toContain("This connection cannot control terminals."); |
| 309 | await act(async () => { |
| 310 | button("Install").props.onClick(); |
| 311 | button("Sign in").props.onClick(); |
| 312 | }); |
| 313 | expect(state.open).not.toHaveBeenCalled(); |
| 314 | expect(state.write).not.toHaveBeenCalled(); |
| 315 | expect(state.close).not.toHaveBeenCalled(); |
| 316 | await click("Continue"); |
| 317 | await click("Do not import projects"); |
| 318 | expect(state.complete).toHaveBeenCalledOnce(); |
| 319 | expect(state.done).toHaveBeenCalledExactlyOnceWith(undefined); |
| 320 | }); |
| 321 | |
| 322 | it("rejects a retained install action after revocation and allows a terminal-only regrant", async () => { |
| 323 | setAccess(primaryId, false); |
| 324 | await enterRemoteAgents(); |
| 325 | const install = button("Install").props.onClick; |
| 326 | await act(async () => { |
| 327 | setAccess(remoteId, false); |
| 328 | install(); |
| 329 | }); |
| 330 | expect(state.open).not.toHaveBeenCalled(); |
| 331 | expect(button("Install").props.disabled).toBe(true); |
| 332 | await act(async () => setAccess(remoteId, true)); |
| 333 | expect(button("Install").props.disabled).toBe(false); |
| 334 | await act(async () => install()); |
| 335 | expect(state.open).toHaveBeenCalledExactlyOnceWith({ |
| 336 | environmentId: remoteId, |
| 337 | input: { |
| 338 | threadId: "onboarding-agent-setup", |
| 339 | terminalId: expect.any(String), |
| 340 | cwd: "/fixtures/paired-remote", |
| 341 | providerInstanceId: missingClaude.instanceId, |
| 342 | }, |
| 343 | }); |
| 344 | expect(state.write).toHaveBeenCalledExactlyOnceWith({ |
| 345 | environmentId: remoteId, |
| 346 | input: { |
| 347 | threadId: "onboarding-agent-setup", |
| 348 | terminalId: state.open.mock.calls[0]![0].input.terminalId, |
| 349 | data: "curl -fsSL https://claude.ai/install.sh | bash", |
| 350 | }, |
| 351 | }); |
| 352 | expect(hasViewport()).toBe(true); |
| 353 | await click("Close"); |
| 354 | expect(state.close).toHaveBeenCalledExactlyOnceWith({ |
| 355 | environmentId: remoteId, |
| 356 | input: { |
| 357 | threadId: "onboarding-agent-setup", |
| 358 | terminalId: state.open.mock.calls[0]![0].input.terminalId, |
| 359 | deleteHistory: true, |
| 360 | }, |
| 361 | }); |
| 362 | }); |
| 363 | |
| 364 | it("pretypes the selected provider's sign-in command without executing it", async () => { |
| 365 | setAccess(primaryId, false); |
| 366 | await enterRemoteAgents(); |
| 367 | await click("Sign in"); |
| 368 | expect(state.open).toHaveBeenCalledExactlyOnceWith({ |
| 369 | environmentId: remoteId, |
| 370 | input: { |
| 371 | threadId: "onboarding-agent-setup", |
| 372 | terminalId: expect.any(String), |
| 373 | cwd: "/fixtures/paired-remote", |
| 374 | providerInstanceId: signedOutCodex.instanceId, |
| 375 | }, |
| 376 | }); |
| 377 | expect(state.write).toHaveBeenCalledExactlyOnceWith({ |
| 378 | environmentId: remoteId, |
| 379 | input: { |
| 380 | threadId: "onboarding-agent-setup", |
| 381 | terminalId: state.open.mock.calls[0]![0].input.terminalId, |
| 382 | data: "/opt/codex-work login", |
| 383 | }, |
| 384 | }); |
| 385 | expect(text(renderer!.root)).toContain("Review the command, then press Enter to run it."); |
| 386 | }); |
| 387 | |
| 388 | it("checks access when a queued retry starts and recovers after regrant", async () => { |
| 389 | state.open.mockResolvedValueOnce(AsyncResult.failure(Cause.fail(new Error("Could not open")))); |
| 390 | await enterRemoteAgents(); |
| 391 | await click("Install"); |
| 392 | const closing = deferred<CommandResult>(); |
| 393 | state.close.mockReturnValueOnce(closing.promise); |
| 394 | await click("Retry"); |
| 395 | expect(state.close).toHaveBeenCalledOnce(); |
| 396 | expect(state.open).toHaveBeenCalledOnce(); |
| 397 | await act(async () => { |
| 398 | setAccess(remoteId, false); |
| 399 | closing.resolve(AsyncResult.success(undefined)); |
| 400 | }); |
| 401 | expect(state.open).toHaveBeenCalledOnce(); |
| 402 | expect(button("Retry").props.disabled).toBe(true); |
| 403 | expect(state.write).not.toHaveBeenCalled(); |
| 404 | await act(async () => setAccess(remoteId, true)); |
| 405 | await click("Retry"); |
| 406 | expect(state.open).toHaveBeenCalledTimes(2); |
| 407 | expect(state.write).toHaveBeenCalledOnce(); |
| 408 | expect(hasViewport()).toBe(true); |
| 409 | }); |
| 410 | |
| 411 | it("keeps an accepted terminal visible without pretyping when access is revoked during open", async () => { |
| 412 | const opening = deferred<CommandResult>(); |
| 413 | state.open.mockReturnValueOnce(opening.promise); |
| 414 | await enterRemoteAgents(); |
| 415 | await click("Install"); |
| 416 | await act(async () => { |
| 417 | setAccess(remoteId, false); |
| 418 | opening.resolve(AsyncResult.success(undefined)); |
| 419 | }); |
| 420 | expect(state.write).not.toHaveBeenCalled(); |
| 421 | expect(state.close).not.toHaveBeenCalled(); |
| 422 | expect(hasViewport()).toBe(true); |
| 423 | expect(text(renderer!.root)).toContain("This connection cannot control terminals."); |
| 424 | await act(async () => setAccess(remoteId, true)); |
| 425 | expect(state.open).toHaveBeenCalledOnce(); |
| 426 | expect(state.write).not.toHaveBeenCalled(); |
| 427 | expect(text(renderer!.root)).toContain( |
| 428 | "Run curl -fsSL https://claude.ai/install.sh | bash in this", |
| 429 | ); |
| 430 | await act(async () => setAccess(remoteId, false)); |
| 431 | await click("Close"); |
| 432 | expect(hasViewport()).toBe(false); |
| 433 | expect(state.close).not.toHaveBeenCalled(); |
| 434 | await click("Continue"); |
| 435 | expect(text(renderer!.root)).toContain("Import your projects"); |
| 436 | }); |
| 437 | |
| 438 | it("settles accepted pretyping locally after revocation without closing the PTY", async () => { |
| 439 | const writing = deferred<CommandResult>(); |
| 440 | state.write.mockReturnValueOnce(writing.promise); |
| 441 | await enterRemoteAgents(); |
| 442 | await click("Sign in"); |
| 443 | await act(async () => { |
| 444 | setAccess(remoteId, false); |
| 445 | writing.resolve(AsyncResult.success(undefined)); |
| 446 | }); |
| 447 | expect(state.open).toHaveBeenCalledOnce(); |
| 448 | expect(state.write).toHaveBeenCalledOnce(); |
| 449 | expect(hasViewport()).toBe(true); |
| 450 | await act(async () => setAccess(remoteId, true)); |
| 451 | expect(text(renderer!.root)).toContain("Review the command, then press Enter to run it."); |
| 452 | expect(state.open).toHaveBeenCalledOnce(); |
| 453 | expect(state.write).toHaveBeenCalledOnce(); |
| 454 | await act(async () => setAccess(remoteId, false)); |
| 455 | await click("Close"); |
| 456 | expect(state.close).not.toHaveBeenCalled(); |
| 457 | }); |
| 458 | |
| 459 | it("rechecks the original environment when cleanup runs after pending pretyping", async () => { |
| 460 | const writing = deferred<CommandResult>(); |
| 461 | state.write.mockReturnValueOnce(writing.promise); |
| 462 | await enterRemoteAgents(); |
| 463 | await click("Install"); |
| 464 | await click("Back"); |
| 465 | expect(state.close).not.toHaveBeenCalled(); |
| 466 | await act(async () => { |
| 467 | setAccess(remoteId, false); |
| 468 | writing.resolve(AsyncResult.success(undefined)); |
| 469 | }); |
| 470 | expect(state.close).not.toHaveBeenCalled(); |
| 471 | expect(text(renderer!.root)).toContain("Connect your computers"); |
| 472 | }); |
| 473 | }); |