Skip to content

Commit a6de7da

Browse files
authored
test(shared): add query-client hotload compatibility regression (#8618)
1 parent e538525 commit a6de7da

2 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import type { QueryClient } from '@tanstack/query-core';
2+
import { QueryClient as TanstackQueryClient } from '@tanstack/query-core';
3+
import { renderHook, waitFor } from '@testing-library/react';
4+
import React from 'react';
5+
import { afterEach, describe, expect, it, vi } from 'vitest';
6+
7+
const mockState = vi.hoisted(() => ({
8+
clerk: undefined as any,
9+
user: undefined as any,
10+
}));
11+
12+
vi.mock('../../contexts', () => ({
13+
useAssertWrappedByClerkProvider: () => {},
14+
useClerkInstanceContext: () => mockState.clerk,
15+
useInitialStateContext: () => undefined,
16+
}));
17+
18+
vi.mock('../base/useUserBase', () => ({
19+
useUserBase: () => mockState.user,
20+
}));
21+
22+
// Models pre-PR #8434 @clerk/shared, which reads the hotloaded Clerk.js
23+
// `__internal_queryClient` bridge and waits for `queryClientStatus`.
24+
vi.mock('../../query/use-clerk-query-client', () => {
25+
type RecursiveMock = {
26+
(...args: unknown[]): RecursiveMock;
27+
} & {
28+
readonly [key in string | symbol]: RecursiveMock;
29+
};
30+
31+
function createRecursiveProxy(label: string): RecursiveMock {
32+
const callableTarget = function noop(): void {};
33+
const handler: ProxyHandler<typeof callableTarget> = {
34+
get(_target, prop) {
35+
if (prop === 'then') {
36+
return undefined;
37+
}
38+
if (prop === 'toString') {
39+
return () => `[${label}]`;
40+
}
41+
if (prop === Symbol.toPrimitive) {
42+
return () => 0;
43+
}
44+
return self;
45+
},
46+
apply() {
47+
return self;
48+
},
49+
construct() {
50+
return self as unknown as object;
51+
},
52+
has() {
53+
return false;
54+
},
55+
set() {
56+
return false;
57+
},
58+
};
59+
60+
const self = new Proxy(callableTarget, handler) as unknown as RecursiveMock;
61+
return self;
62+
}
63+
64+
const mockQueryClient = createRecursiveProxy('LegacyClerkMockQueryClient') as unknown as QueryClient;
65+
const isLegacyQueryClient = (value: unknown): value is { __tag: 'clerk-rq-client'; client: QueryClient } =>
66+
typeof value === 'object' &&
67+
value !== null &&
68+
'__tag' in value &&
69+
(value as { __tag?: string }).__tag === 'clerk-rq-client';
70+
71+
const useClerkQueryClient = (): [QueryClient, boolean] => {
72+
const clerk = mockState.clerk;
73+
const queryClient = clerk.__internal_queryClient;
74+
const [queryClientLoaded, setQueryClientLoaded] = React.useState(isLegacyQueryClient(queryClient));
75+
76+
React.useEffect(() => {
77+
const setLoaded = () => setQueryClientLoaded(true);
78+
clerk.on('queryClientStatus', setLoaded);
79+
return () => clerk.off('queryClientStatus', setLoaded);
80+
}, [clerk]);
81+
82+
const isLoaded = queryClientLoaded && isLegacyQueryClient(queryClient);
83+
return [isLoaded ? queryClient.client : mockQueryClient, isLoaded];
84+
};
85+
86+
return { useClerkQueryClient };
87+
});
88+
89+
import { useOrganizationList } from '../useOrganizationList';
90+
91+
function createHotloadedClerkQueryClientShim() {
92+
const listeners = new Set<(status: 'ready') => void>();
93+
let isResolving = false;
94+
let queryClient: { __tag: 'clerk-rq-client'; client: TanstackQueryClient } | undefined;
95+
96+
return {
97+
loaded: true,
98+
telemetry: { record: vi.fn() },
99+
setActive: vi.fn(),
100+
createOrganization: vi.fn(),
101+
// Reached transitively via useAttemptToEnableOrganizations.
102+
__internal_attemptToEnableEnvironmentSetting: vi.fn(),
103+
get __internal_queryClient() {
104+
if (!queryClient && !isResolving) {
105+
isResolving = true;
106+
void Promise.resolve().then(() => {
107+
queryClient = {
108+
__tag: 'clerk-rq-client',
109+
client: new TanstackQueryClient({
110+
defaultOptions: {
111+
queries: {
112+
retry: false,
113+
staleTime: Infinity,
114+
refetchOnMount: false,
115+
refetchOnReconnect: false,
116+
refetchOnWindowFocus: false,
117+
},
118+
},
119+
}),
120+
};
121+
listeners.forEach(listener => listener('ready'));
122+
});
123+
}
124+
return queryClient;
125+
},
126+
on: vi.fn((event: string, listener: (status: 'ready') => void) => {
127+
if (event === 'queryClientStatus') {
128+
listeners.add(listener);
129+
}
130+
}),
131+
off: vi.fn((event: string, listener: (status: 'ready') => void) => {
132+
if (event === 'queryClientStatus') {
133+
listeners.delete(listener);
134+
}
135+
}),
136+
};
137+
}
138+
139+
afterEach(() => {
140+
vi.clearAllMocks();
141+
mockState.clerk = undefined;
142+
mockState.user = undefined;
143+
});
144+
145+
describe('useOrganizationList hotload compatibility', () => {
146+
it('leaves the legacy mock query-client state and requests organization memberships', async () => {
147+
const fapiRequest = vi.fn((_request: unknown) =>
148+
Promise.resolve({
149+
data: [],
150+
total_count: 0,
151+
}),
152+
);
153+
const membershipRequest = vi.fn((params?: { initialPage?: number; pageSize?: number }) => {
154+
return fapiRequest({
155+
path: '/me/organization_memberships',
156+
method: 'GET',
157+
search: params,
158+
});
159+
});
160+
161+
mockState.clerk = createHotloadedClerkQueryClientShim();
162+
mockState.user = {
163+
id: 'user_123',
164+
getOrganizationMemberships: membershipRequest,
165+
};
166+
167+
const { result } = renderHook(() =>
168+
useOrganizationList({
169+
userMemberships: {
170+
pageSize: 2,
171+
},
172+
}),
173+
);
174+
175+
expect(result.current.isLoaded).toBe(true);
176+
expect(result.current.userMemberships.isLoading).toBe(true);
177+
expect(membershipRequest).not.toHaveBeenCalled();
178+
179+
await waitFor(() => expect(membershipRequest).toHaveBeenCalledTimes(1));
180+
expect(membershipRequest).toHaveBeenCalledWith({ initialPage: 1, pageSize: 2 });
181+
expect(fapiRequest).toHaveBeenCalledWith({
182+
path: '/me/organization_memberships',
183+
method: 'GET',
184+
search: { initialPage: 1, pageSize: 2 },
185+
});
186+
await waitFor(() => expect(result.current.userMemberships.isLoading).toBe(false));
187+
188+
expect(result.current.userMemberships.isFetching).toBe(false);
189+
expect(result.current.userMemberships.count).toBe(0);
190+
});
191+
});

0 commit comments

Comments
 (0)