Skip to content
Source
Sign inSign up

usePullRequestList.test.tsx

138 lines4.6 KB

1import { RegistryContext } from "@effect/atom-react";
2import {
3 EnvironmentId,
4 type PullRequestListInput,
5 type PullRequestListResult,
6} from "@t3tools/contracts";
7import { AtomRegistry } from "effect/reactivity";
8import { act, useLayoutEffect } from "react";
9import { createRoot } from "react-dom/client";
10import { expect, it, vi } from "vite-plus/test";
11​
12const mocks = vi.hoisted(() => ({ failPage: false }));
13​
14vi.mock("../../connection/catalog", async () => {
15 const { Atom } = await import("effect/reactivity");
16 return {
17 environmentCatalog: {
18 sessionGenerationAtom: Atom.family((_environmentId: string) => Atom.make(0)),
19 },
20 };
21});
22​
23vi.mock("../../state/pull-requests", async () => {
24 const { AsyncResult, Atom } = await import("effect/reactivity");
25 const Effect = await import("effect/Effect");
26 const Schema = await import("effect/Schema");
27 class PullRequestListTestError extends Schema.TaggedError<PullRequestListTestError>()(
28 "PullRequestListTestError",
29 { message: Schema.String },
30 ) {}
31 const page = (nextCursors: PullRequestListResult["nextCursors"]): PullRequestListResult => ({
32 entries: [],
33 nextCursors,
34 truncated: false,
35 viewers: {},
36 providers: [],
37 errors: [],
38 });
39 const list = Atom.family((key: string) => {
40 const target: { environmentId: string; input: PullRequestListInput } = JSON.parse(key);
41 const response = page(target.input.cursors ? {} : { "github.com org/repo": "next" });
42 return Atom.make(
43 Effect.suspend(() => {
44 if (target.environmentId === "offline")
45 return Effect.fail(
46 new PullRequestListTestError({ message: "Environment is not connected." }),
47 );
48 if (target.input.cursors && mocks.failPage)
49 return Effect.fail(new PullRequestListTestError({ message: "Page failed." }));
50 return Effect.succeed(response);
51 }),
52 );
53 });
54 const { pullRequestListHead } = await import("./pullRequestList");
55 const pages = Atom.family((_key: string) =>
56 Atom.make<ReturnType<typeof pullRequestListHead> | null>(null),
57 );
58 return {
59 pullRequestListPagesKey: (target: unknown) => JSON.stringify(target),
60 pullRequestListPagesFamily: pages,
61 pullRequestListStatsFamily: Atom.family((_key: string) => Atom.make(new Map())),
62 composerPullRequests: {
63 list: (target: unknown) => list(JSON.stringify(target)),
64 listStats: Atom.family(() => Atom.make(AsyncResult.success({ stats: [] }))),
65 invalidate: {
66 run: async () => AsyncResult.success({}),
67 },
68 },
69 };
70});
71​
72import { usePullRequestList } from "./usePullRequestList";
73​
74function setup(ids: readonly string[]) {
75 mocks.failPage = false;
76 (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
77 const registry = AtomRegistry.make();
78 const targets = ids.map((id) => ({
79 environmentId: EnvironmentId.make(id),
80 input: { state: "open" as const },
81 }));
82 let latest: ReturnType<typeof usePullRequestList>;
83 function Probe() {
84 const result = usePullRequestList(targets);
85 useLayoutEffect(() => {
86 latest = result;
87 });
88 return null;
89 }
90 const root = createRoot(document.createElement("div"));
91 act(() => {
92 root.render(
93 <RegistryContext.Provider value={registry}>
94 <Probe />
95 </RegistryContext.Provider>,
96 );
97 });
98 return {
99 get latest() {
100 return latest;
101 },
102 [Symbol.dispose]() {
103 act(() => root.unmount());
104 registry.dispose();
105 },
106 };
108​
109it("keeps a refresh failure in the source errors and allows healthy pagination", async () => {
110 using probe = setup(["healthy", "offline"]);
111 await act(async () => probe.latest.onRefresh());
112 expect(probe.latest.data.errors.map((error) => error.message)).toEqual([
113 "Environment is not connected.",
114 ]);
115 expect(probe.latest.refreshing).toBe(false);
116 expect(probe.latest.loadMoreError).toBeNull();
117​
118 await act(async () => probe.latest.loadMore());
119 expect(probe.latest.continued).toBe(true);
120 expect(probe.latest.hasMore).toBe(false);
121 expect(probe.latest.loadMoreError).toBeNull();
122});
123​
124it("keeps load-more failures retryable", async () => {
125 using probe = setup(["healthy"]);
126 await act(async () => probe.latest.onRefresh());
127 mocks.failPage = true;
128 await act(async () => probe.latest.loadMore());
129 expect(probe.latest.loadMoreError).toBe("Page failed.");
130 expect(probe.latest.hasMore).toBe(true);
131 expect(probe.latest.continued).toBe(false);
132​
133 mocks.failPage = false;
134 await act(async () => probe.latest.loadMore(true));
135 expect(probe.latest.loadMoreError).toBeNull();
136 expect(probe.latest.hasMore).toBe(false);
137 expect(probe.latest.continued).toBe(true);
138});