Skip to content

Commit 4a84f3e

Browse files
committed
feat(react): add context, provider and hooks test suites
1 parent 3f29b44 commit 4a84f3e

18 files changed

+3848
-9
lines changed

packages/sdk-react/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@
4949
"@rollup/plugin-node-resolve": "^16.0.0",
5050
"@rollup/plugin-typescript": "^12.0.0",
5151
"@tanstack/react-query": "^5.0.0",
52+
"@testing-library/jest-dom": "^6.0.0",
5253
"@testing-library/react": "^16.0.0",
5354
"@types/react": "^18.0.0",
5455
"@vitest/coverage-v8": "^3.0.0",
5556
"@vitest/ui": "^3.0.0",
57+
"jsdom": "^26.0.0",
5658
"react": "^18.0.0",
5759
"react-dom": "^18.0.0",
5860
"rollup": "^4.0.0",
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Tests for ATProtoProvider component.
3+
*/
4+
5+
import { describe, expect, it } from "vitest";
6+
import { render, screen } from "@testing-library/react";
7+
import { QueryClientProvider } from "@tanstack/react-query";
8+
import React, { useContext } from "react";
9+
import type { ATProtoSDK } from "@hypercerts-org/sdk-core";
10+
import { ATProtoProvider } from "../../src/context/ATProtoProvider.js";
11+
import { ATProtoContext } from "../../src/context/ATProtoContext.js";
12+
import type { ATProtoContextValue } from "../../src/context/types.js";
13+
import { TestProvider } from "../../src/testing/TestProvider.js";
14+
import { createTestQueryClient } from "../utils/render.js";
15+
import { createMockSession } from "../utils/fixtures.js";
16+
17+
describe("ATProtoProvider", () => {
18+
it("should render children", () => {
19+
render(
20+
<TestProvider>
21+
<div data-testid="child">Test Child</div>
22+
</TestProvider>,
23+
);
24+
25+
expect(screen.getByTestId("child")).toHaveTextContent("Test Child");
26+
});
27+
28+
it("should provide context value to children", () => {
29+
const session = createMockSession();
30+
let contextValue: ATProtoContextValue | null = null;
31+
32+
const ContextConsumer = () => {
33+
contextValue = useContext(ATProtoContext);
34+
return <div data-testid="consumer">Consumer</div>;
35+
};
36+
37+
render(
38+
<TestProvider mockSession={session}>
39+
<ContextConsumer />
40+
</TestProvider>,
41+
);
42+
43+
expect(contextValue).not.toBeNull();
44+
expect(contextValue!.sdk).toBeDefined();
45+
expect(contextValue!.queryClient).toBeDefined();
46+
expect(contextValue!.initialSession).toBe(session);
47+
});
48+
49+
it("should accept dehydratedState for SSR hydration", () => {
50+
const queryClient = createTestQueryClient();
51+
const dehydratedState = { mutations: [], queries: [] };
52+
53+
// Should not throw when using ATProtoProvider directly with dehydratedState
54+
render(
55+
<QueryClientProvider client={queryClient}>
56+
<ATProtoProvider
57+
value={{
58+
sdk: {} as ATProtoSDK,
59+
queryClient,
60+
initialSession: null,
61+
syncTabs: false,
62+
}}
63+
dehydratedState={dehydratedState}
64+
>
65+
<div>Test</div>
66+
</ATProtoProvider>
67+
</QueryClientProvider>,
68+
);
69+
});
70+
});
71+
72+
describe("ATProtoContext", () => {
73+
it("should be null when used outside provider", () => {
74+
let contextValue: ATProtoContextValue | null | string = "not-null";
75+
76+
const ContextConsumer = () => {
77+
contextValue = useContext(ATProtoContext);
78+
return <div>Consumer</div>;
79+
};
80+
81+
render(<ContextConsumer />);
82+
83+
expect(contextValue).toBeNull();
84+
});
85+
});
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* Tests for createATProtoReact factory function.
3+
*/
4+
5+
import { describe, expect, it, vi } from "vitest";
6+
import { QueryClient } from "@tanstack/react-query";
7+
import { render, screen } from "@testing-library/react";
8+
import React from "react";
9+
import { createATProtoReact } from "../../src/factory/createATProtoReact.js";
10+
import { createMockSDKConfig, createMockSession } from "../utils/fixtures.js";
11+
12+
// Mock sdk-core
13+
vi.mock("@hypercerts-org/sdk-core", () => ({
14+
createATProtoSDK: vi.fn(() => ({
15+
authorize: vi.fn(),
16+
callback: vi.fn(),
17+
restoreSession: vi.fn(),
18+
revokeSession: vi.fn(),
19+
getRepository: vi.fn(),
20+
getLexiconRegistry: vi.fn(),
21+
pdsUrl: "https://pds.example.com",
22+
sdsUrl: "https://sds.example.com",
23+
})),
24+
}));
25+
26+
describe("createATProtoReact", () => {
27+
describe("initialization", () => {
28+
it("should create an instance with config", () => {
29+
const config = createMockSDKConfig();
30+
const instance = createATProtoReact({ config });
31+
32+
expect(instance).toBeDefined();
33+
expect(instance.sdk).toBeDefined();
34+
expect(instance.queryClient).toBeDefined();
35+
expect(instance.Provider).toBeDefined();
36+
});
37+
38+
it("should create an instance with existing SDK", () => {
39+
const mockSDK = {
40+
authorize: vi.fn(),
41+
callback: vi.fn(),
42+
restoreSession: vi.fn(),
43+
revokeSession: vi.fn(),
44+
getRepository: vi.fn(),
45+
getLexiconRegistry: vi.fn(),
46+
pdsUrl: "https://pds.example.com",
47+
sdsUrl: "https://sds.example.com",
48+
};
49+
50+
const instance = createATProtoReact({ sdk: mockSDK as any });
51+
52+
expect(instance.sdk).toBe(mockSDK);
53+
});
54+
55+
it("should throw error if neither config nor sdk provided", () => {
56+
expect(() => createATProtoReact({} as any)).toThrow(
57+
"createATProtoReact requires either 'config' or 'sdk' option"
58+
);
59+
});
60+
61+
it("should use provided QueryClient", () => {
62+
const config = createMockSDKConfig();
63+
const customQueryClient = new QueryClient();
64+
const instance = createATProtoReact({ config, queryClient: customQueryClient });
65+
66+
expect(instance.queryClient).toBe(customQueryClient);
67+
});
68+
69+
it("should create default QueryClient if not provided", () => {
70+
const config = createMockSDKConfig();
71+
const instance = createATProtoReact({ config });
72+
73+
expect(instance.queryClient).toBeInstanceOf(QueryClient);
74+
});
75+
76+
it("should configure default QueryClient with staleTime", () => {
77+
const config = createMockSDKConfig();
78+
const instance = createATProtoReact({ config });
79+
80+
// Check default options were set
81+
const defaults = instance.queryClient.getDefaultOptions();
82+
expect(defaults.queries?.staleTime).toBe(5 * 60 * 1000);
83+
expect(defaults.queries?.retry).toBe(1);
84+
});
85+
});
86+
87+
describe("returned instance", () => {
88+
it("should expose SDK instance", () => {
89+
const config = createMockSDKConfig();
90+
const instance = createATProtoReact({ config });
91+
92+
expect(instance.sdk).toBeDefined();
93+
});
94+
95+
it("should expose QueryClient", () => {
96+
const config = createMockSDKConfig();
97+
const instance = createATProtoReact({ config });
98+
99+
expect(instance.queryClient).toBeInstanceOf(QueryClient);
100+
});
101+
102+
it("should expose Provider component", () => {
103+
const config = createMockSDKConfig();
104+
const instance = createATProtoReact({ config });
105+
106+
expect(instance.Provider).toBeDefined();
107+
expect(typeof instance.Provider).toBe("function");
108+
});
109+
110+
it("should expose all hooks", () => {
111+
const config = createMockSDKConfig();
112+
const instance = createATProtoReact({ config });
113+
114+
expect(instance.useSDK).toBeDefined();
115+
expect(instance.useAuth).toBeDefined();
116+
expect(instance.useRepository).toBeDefined();
117+
expect(instance.useProfile).toBeDefined();
118+
expect(instance.useOrganizations).toBeDefined();
119+
expect(instance.useOrganization).toBeDefined();
120+
expect(instance.useCollaborators).toBeDefined();
121+
expect(instance.useHypercerts).toBeDefined();
122+
expect(instance.useHypercert).toBeDefined();
123+
});
124+
125+
it("should expose queryKeys", () => {
126+
const config = createMockSDKConfig();
127+
const instance = createATProtoReact({ config });
128+
129+
expect(instance.queryKeys).toBeDefined();
130+
expect(instance.queryKeys.all).toEqual(["atproto"]);
131+
});
132+
});
133+
134+
describe("Provider component", () => {
135+
it("should render children", () => {
136+
const config = createMockSDKConfig();
137+
const instance = createATProtoReact({ config });
138+
const { Provider, queryClient } = instance;
139+
140+
render(
141+
<QueryClientProvider client={queryClient}>
142+
<Provider>
143+
<div data-testid="child">Hello</div>
144+
</Provider>
145+
</QueryClientProvider>
146+
);
147+
148+
expect(screen.getByTestId("child")).toHaveTextContent("Hello");
149+
});
150+
151+
it("should accept dehydratedState prop", () => {
152+
const config = createMockSDKConfig();
153+
const instance = createATProtoReact({ config });
154+
const { Provider, queryClient } = instance;
155+
156+
// Should not throw with dehydratedState
157+
render(
158+
<QueryClientProvider client={queryClient}>
159+
<Provider dehydratedState={{ mutations: [], queries: [] }}>
160+
<div>Test</div>
161+
</Provider>
162+
</QueryClientProvider>
163+
);
164+
});
165+
});
166+
167+
describe("options", () => {
168+
it("should accept initialSession option", () => {
169+
const config = createMockSDKConfig();
170+
const session = createMockSession();
171+
const instance = createATProtoReact({ config, initialSession: session });
172+
173+
expect(instance).toBeDefined();
174+
});
175+
176+
it("should accept syncTabs option", () => {
177+
const config = createMockSDKConfig();
178+
const instance = createATProtoReact({ config, syncTabs: false });
179+
180+
expect(instance).toBeDefined();
181+
});
182+
183+
it("should default syncTabs to true", () => {
184+
const config = createMockSDKConfig();
185+
const instance = createATProtoReact({ config });
186+
187+
expect(instance).toBeDefined();
188+
// syncTabs defaults to true internally
189+
});
190+
});
191+
});
192+
193+
// Need to import QueryClientProvider for the Provider tests
194+
import { QueryClientProvider } from "@tanstack/react-query";

0 commit comments

Comments
 (0)