1// @vitest-environment jsdom
2
3import { FunctionalComponent as FC, h } from 'preact';
4import { render, cleanup, act } from '@testing-library/preact';
5import { OperationContext } from '@urql/core';
6import { map, interval, pipe, never, onStart, onEnd, empty } from 'wonka';
7
8import { vi, expect, it, beforeEach, describe, afterEach, Mock } from 'vitest';
9
10import { useQuery, UseQueryArgs, UseQueryState } from './useQuery';
11import { Provider } from '../context';
12
13const mock = {
14 executeQuery: vi.fn(() =>
15 pipe(
16 interval(400),
17 map((i: number) => ({ data: i, error: i + 1, extensions: { i: 1 } }))
18 )
19 ),
20};
21
22const client = mock as { executeQuery: Mock };
23const props: UseQueryArgs<{ myVar: number }> = {
24 query: '{ example }',
25 variables: {
26 myVar: 1234,
27 },
28 pause: false,
29};
30
31let state: UseQueryState<any> | undefined;
32let execute: ((_opts?: Partial<OperationContext>) => void) | undefined;
33
34const QueryUser: FC<UseQueryArgs<{ myVar: number }>> = ({
35 query,
36 variables,
37 pause,
38}) => {
39 [state, execute] = useQuery({ query, variables, pause });
40 return h('p', {}, state.data);
41};
42
43beforeEach(() => {
44 vi.useFakeTimers();
45 vi.spyOn(globalThis.console, 'error');
46});
47
48describe('useQuery', () => {
49 beforeEach(() => {
50 client.executeQuery.mockClear();
51 state = undefined;
52 execute = undefined;
53 });
54
55 afterEach(() => cleanup());
56
57 it('executes subscription', () => {
58 render(
59 h(Provider, {
60 value: client as any,
61 children: [h(QueryUser, { ...props })],
62 })
63 );
64 expect(client.executeQuery).toBeCalledTimes(1);
65 });
66
67 it('passes query and vars to executeQuery', () => {
68 render(
69 h(Provider, {
70 value: client as any,
71 children: [h(QueryUser, { ...props })],
72 })
73 );
74
75 expect(client.executeQuery).toBeCalledWith(
76 {
77 key: expect.any(Number),
78 query: expect.any(Object),
79 variables: props.variables,
80 },
81 expect.objectContaining({
82 requestPolicy: undefined,
83 })
84 );
85 });
86
87 it('sets fetching to true', () => {
88 const { rerender } = render(
89 h(Provider, {
90 value: client as any,
91 children: [h(QueryUser, { ...props })],
92 })
93 );
94
95 rerender(
96 h(Provider, {
97 value: client as any,
98 children: [h(QueryUser, { ...props })],
99 })
100 );
101 expect(state).toHaveProperty('fetching', true);
102 });
103
104 it('forwards data response', () => {
105 const { rerender } = render(
106 h(Provider, {
107 value: client as any,
108 children: [h(QueryUser, { ...props })],
109 })
110 );
111
112 rerender(
113 h(Provider, {
114 value: client as any,
115 children: [h(QueryUser, { ...props })],
116 })
117 );
118
119 act(() => {
120 vi.advanceTimersByTime(400);
121 rerender(
122 h(Provider, {
123 value: client as any,
124 children: [h(QueryUser, { ...props })],
125 })
126 );
127 });
128
129 expect(state).toHaveProperty('data', 0);
130 });
131
132 it('forwards error response', () => {
133 const { rerender } = render(
134 h(Provider, {
135 value: client as any,
136 children: [h(QueryUser, { ...props })],
137 })
138 );
139
140 rerender(
141 h(Provider, {
142 value: client as any,
143 children: [h(QueryUser, { ...props })],
144 })
145 );
146
147 act(() => {
148 vi.advanceTimersByTime(400);
149 rerender(
150 h(Provider, {
151 value: client as any,
152 children: [h(QueryUser, { ...props })],
153 })
154 );
155 });
156
157 expect(state).toHaveProperty('error', 1);
158 });
159
160 it('forwards extensions response', () => {
161 const { rerender } = render(
162 h(Provider, {
163 value: client as any,
164 children: [h(QueryUser, { ...props })],
165 })
166 );
167
168 rerender(
169 h(Provider, {
170 value: client as any,
171 children: [h(QueryUser, { ...props })],
172 })
173 );
174
175 act(() => {
176 vi.advanceTimersByTime(400);
177 rerender(
178 h(Provider, {
179 value: client as any,
180 children: [h(QueryUser, { ...props })],
181 })
182 );
183 });
184
185 expect(state).toHaveProperty('extensions', { i: 1 });
186 });
187
188 it('sets fetching to false', () => {
189 const { rerender } = render(
190 h(Provider, {
191 value: client as any,
192 children: [h(QueryUser, { ...props })],
193 })
194 );
195
196 rerender(
197 h(Provider, {
198 value: client as any,
199 children: [h(QueryUser, { ...props })],
200 })
201 );
202
203 act(() => {
204 vi.advanceTimersByTime(400);
205 rerender(
206 h(Provider, {
207 value: client as any,
208 children: [h(QueryUser, { ...props })],
209 })
210 );
211 });
212
213 expect(state).toHaveProperty('fetching', false);
214 });
215
216 describe('on change', () => {
217 const q = 'query NewQuery { example }';
218
219 it('new query executes subscription', () => {
220 const { rerender } = render(
221 h(Provider, {
222 value: client as any,
223 children: [h(QueryUser, { ...props })],
224 })
225 );
226
227 rerender(
228 h(Provider, {
229 value: client as any,
230 children: [h(QueryUser, { ...props, query: q })],
231 })
232 );
233
234 act(() => {
235 rerender(
236 h(Provider, {
237 value: client as any,
238 children: [h(QueryUser, { ...props, query: q })],
239 })
240 );
241 });
242
243 expect(client.executeQuery).toBeCalledTimes(2);
244 });
245 });
246
247 describe('on unmount', () => {
248 const start = vi.fn();
249 const unsubscribe = vi.fn();
250
251 beforeEach(() => {
252 client.executeQuery.mockReturnValue(
253 pipe(never, onStart(start), onEnd(unsubscribe))
254 );
255 });
256
257 it('unsubscribe is called', () => {
258 const { unmount } = render(
259 h(Provider, {
260 value: client as any,
261 children: [h(QueryUser, { ...props })],
262 })
263 );
264
265 act(() => {
266 unmount();
267 });
268
269 expect(start).toBeCalledTimes(2);
270 expect(unsubscribe).toBeCalledTimes(2);
271 });
272 });
273
274 describe('active teardown', () => {
275 it('sets fetching to false when the source ends', () => {
276 client.executeQuery.mockReturnValueOnce(empty);
277 act(() => {
278 render(
279 h(Provider, {
280 value: client as any,
281 children: [h(QueryUser, { ...props })],
282 })
283 );
284 });
285 expect(client.executeQuery).toHaveBeenCalled();
286 expect(state).toMatchObject({ fetching: false });
287 });
288 });
289
290 describe('execute query', () => {
291 it('triggers query execution', () => {
292 render(
293 h(Provider, {
294 value: client as any,
295 children: [h(QueryUser, { ...props })],
296 })
297 );
298 act(() => execute && execute());
299 expect(client.executeQuery).toBeCalledTimes(2);
300 });
301 });
302
303 describe('pause', () => {
304 it('skips executing the query if pause is true', () => {
305 render(
306 h(Provider, {
307 value: client as any,
308 children: [h(QueryUser, { ...props, pause: true })],
309 })
310 );
311 expect(client.executeQuery).not.toBeCalled();
312 });
313
314 it('skips executing queries if pause updates to true', () => {
315 const { rerender } = render(
316 h(Provider, {
317 value: client as any,
318 children: [h(QueryUser, { ...props })],
319 })
320 );
321
322 rerender(
323 h(Provider, {
324 value: client as any,
325 children: [h(QueryUser, { ...props, pause: true })],
326 })
327 );
328
329 expect(client.executeQuery).toBeCalledTimes(1);
330 });
331 });
332});