Skip to content
Source
Sign inSign up
Open History to see the last commit

AddCodexAccountDialog.browser.test.tsx

139 lines4.7 KB

1import {
2 EnvironmentId,
3 ProviderDriverKind,
4 ProviderInstanceId,
5 type ServerProvider,
6} from "@t3tools/contracts";
7import { Atom } from "effect/reactivity";
8import { useState } from "react";
9import { page, userEvent } from "vite-plus/test/browser";
10import { describe, expect, it, vi } from "vite-plus/test";
11​
12import { act, render } from "../../../test/render";
13import { AppAtomRegistryProvider, appAtomRegistry as registry } from "../../rpc/atomRegistry";
14​
15const setup = vi.hoisted(() => ({
16 update: vi.fn(async () => ({ _tag: "Success" as const })),
17}));
18​
19const providersAtom = vi.hoisted(() => ({ current: null as unknown }));
20​
21vi.mock(import("../../state/server"), async (original) => {
22 const actual = await original();
23 return {
24 ...actual,
25 serverEnvironment: {
26 ...actual.serverEnvironment,
27 providersValueAtom: (() => providersAtom.current) as never,
28 updateSettings: setup.update as never,
29 },
30 };
31});
32vi.mock(import("../../state/use-atom-command"), async (original) => ({
33 ...(await original()),
34 useAtomCommand: ((command: unknown) => command) as never,
35}));
36vi.mock(import("../../state/session"), async (original) => ({
37 ...(await original()),
38 useEnvironmentScope: () => true,
39 readEnvironmentScope: () => true,
40}));
41vi.mock(import("../../hooks/useSettings"), async (original) => ({
42 ...(await original()),
43 useEnvironmentSettings: (() => ({ providerInstances: {} })) as never,
44}));
45​
46import { AddCodexAccountDialog } from "./AddCodexAccountDialog";
47​
48const environmentId = EnvironmentId.make("local");
49​
50function codexProvider(
51 instanceId: ProviderInstanceId,
52 auth: ServerProvider["auth"],
53): ServerProvider {
54 return {
55 instanceId,
56 driver: ProviderDriverKind.make("codex"),
57 installed: true,
58 enabled: true,
59 version: "test-version",
60 status: "ready",
61 auth,
62 checkedAt: "2026-09-02T00:00:00.000Z",
63 models: [],
64 skills: [],
65 slashCommands: [],
66 setup: { canAuthenticate: true, canInstall: false },
67 };
69​
70function createdInstanceId(): ProviderInstanceId {
71 const [call] = setup.update.mock.calls as unknown as [
72 [{ input: { patch: { providerInstances: Record<string, unknown> } } }],
73 ];
74 return ProviderInstanceId.make(Object.keys(call[0].input.patch.providerInstances)[0]!);
76​
77function Owner({
78 onClose,
79 onAccountCreated,
80}: {
81 readonly onClose: () => void;
82 readonly onAccountCreated: (instanceId: ProviderInstanceId) => void;
83}) {
84 const [shown, setShown] = useState(true);
85 return shown ? (
86 <AddCodexAccountDialog
87 environmentId={environmentId}
88 onClose={() => {
89 onClose();
90 setShown(false);
91 }}
92 onAccountCreated={onAccountCreated}
93 renderSetup={() => <p>Sign in to ChatGPT</p>}
94 />
95 ) : null;
97​
98describe("AddCodexAccountDialog", () => {
99 it("plays its close animation before the owner unmounts it once the account connects", async () => {
100 const atom = Atom.make<ReadonlyArray<ServerProvider> | null>([]).pipe(Atom.keepAlive);
101 providersAtom.current = atom;
102 const onClose = vi.fn();
103 const onAccountCreated = vi.fn();
104 // A long exit transition that the test finishes by hand, so the close is
105 // observable mid-animation without waiting on real time.
106 const style = document.createElement("style");
107 style.textContent = `[data-slot="dialog-popup"] { transition: opacity 600s; }
108 [data-slot="dialog-popup"][data-ending-style] { opacity: 0; }`;
109 document.head.append(style);
110 using _style = { [Symbol.dispose]: () => style.remove() };
111 await using _screen = await render(
112 <AppAtomRegistryProvider>
113 <Owner onClose={onClose} onAccountCreated={onAccountCreated} />
114 </AppAtomRegistryProvider>,
115 );
116​
117 await userEvent.click(page.getByRole("button", { name: "Continue" }));
118 const instanceId = createdInstanceId();
119 await act(() => registry.set(atom, [codexProvider(instanceId, { status: "unauthenticated" })]));
120 // Telling the owner about the account does not skip the dialog's own sign-in.
121 await expect.element(page.getByText("Sign in to ChatGPT")).toBeVisible();
122​
123 await act(() =>
124 registry.set(atom, [
125 codexProvider(instanceId, { status: "authenticated", subscriptionSharing: true }),
126 ]),
127 );
128 // Closing the dialog starts its exit animation; unmounting now would cut it off.
129 await vi.waitFor(() => expect(document.getAnimations()).not.toHaveLength(0));
130 expect(onClose).not.toHaveBeenCalled();
131​
132 await act(() => {
133 for (const animation of document.getAnimations()) animation.finish();
134 });
135 await vi.waitFor(() => expect(onClose).toHaveBeenCalledTimes(1));
136 // onClose runs before the owner's unmount commits, so wait for the dialog to leave the DOM.
137 await expect.element(page.getByRole("dialog")).not.toBeInTheDocument();
138 });
139});