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