1import {
2 OperationResult,
3 OperationResultSource,
4 RequestPolicy,
5} from '@urql/core';
6import { computed, nextTick, reactive, readonly, ref } from 'vue';
7import { vi, expect, it, describe } from 'vitest';
8
9vi.mock('./useClient.ts', async () => ({
10 __esModule: true,
11 ...(await vi.importActual<typeof import('./useClient')>('./useClient.ts')),
12 useClient: () => ref(client),
13}));
14
15import { pipe, makeSubject, fromValue, delay } from 'wonka';
16import { createClient } from '@urql/core';
17import { useQuery, UseQueryArgs } from './useQuery';
18
19const client = createClient({ url: '/graphql', exchanges: [] });
20
21const createQuery = (args: UseQueryArgs) => {
22 const executeQuery = vi
23 .spyOn(client, 'executeQuery')
24 .mockImplementation(request => {
25 return pipe(
26 fromValue({ operation: request, data: { test: true } }),
27 delay(1)
28 ) as any;
29 });
30
31 const query$ = useQuery(args);
32
33 return {
34 query$,
35 executeQuery,
36 };
37};
38
39describe('useQuery', () => {
40 it('runs a query and updates data', async () => {
41 const subject = makeSubject<any>();
42 const executeQuery = vi
43 .spyOn(client, 'executeQuery')
44 .mockImplementation(
45 () => subject.source as OperationResultSource<OperationResult>
46 );
47
48 const query = useQuery({
49 query: `{ test }`,
50 });
51
52 expect(readonly(query)).toMatchObject({
53 data: undefined,
54 stale: false,
55 fetching: true,
56 error: undefined,
57 extensions: undefined,
58 operation: undefined,
59 isPaused: false,
60 pause: expect.any(Function),
61 resume: expect.any(Function),
62 executeQuery: expect.any(Function),
63 then: expect.any(Function),
64 });
65
66 expect(executeQuery).toHaveBeenCalledWith(
67 {
68 key: expect.any(Number),
69 query: expect.any(Object),
70 variables: {},
71 context: undefined,
72 },
73 {
74 requestPolicy: undefined,
75 }
76 );
77
78 expect(query.fetching.value).toBe(true);
79
80 subject.next({ data: { test: true } });
81
82 expect(query.fetching.value).toBe(false);
83 expect(query.data.value).toHaveProperty('test', true);
84 });
85
86 it('runs queries as a promise-like that resolves when used', async () => {
87 const executeQuery = vi
88 .spyOn(client, 'executeQuery')
89 .mockImplementation(() => {
90 return pipe(fromValue({ data: { test: true } }), delay(1)) as any;
91 });
92
93 const query = await useQuery({
94 query: `{ test }`,
95 });
96
97 expect(executeQuery).toHaveBeenCalledTimes(1);
98 expect(query.fetching.value).toBe(false);
99 expect(query.data.value).toEqual({ test: true });
100 });
101
102 it('runs queries as a promise-like that resolves even when the query changes', async () => {
103 const doc = ref('{ test }');
104
105 const { executeQuery, query$ } = createQuery({
106 query: doc,
107 });
108
109 doc.value = '{ test2 }';
110
111 await query$;
112
113 expect(executeQuery).toHaveBeenCalledTimes(2);
114 expect(query$.fetching.value).toBe(false);
115 expect(query$.data.value).toEqual({ test: true });
116
117 expect(query$.operation.value).toHaveProperty(
118 'query.definitions.0.selectionSet.selections.0.name.value',
119 'test2'
120 );
121 });
122
123 it('reacts to ref variables changing', async () => {
124 const variables = ref({ prop: 1 });
125
126 const { executeQuery, query$ } = createQuery({
127 query: ref('{ test }'),
128 variables,
129 });
130
131 await query$;
132 expect(executeQuery).toHaveBeenCalledTimes(1);
133 expect(query$.operation.value).toHaveProperty('variables.prop', 1);
134
135 variables.value.prop++;
136 await query$;
137 expect(executeQuery).toHaveBeenCalledTimes(2);
138 expect(query$.operation.value).toHaveProperty('variables.prop', 2);
139
140 variables.value = { prop: 3 };
141 await query$;
142 expect(executeQuery).toHaveBeenCalledTimes(3);
143 expect(query$.operation.value).toHaveProperty('variables.prop', 3);
144 });
145
146 it('reacts to ref variables changing', async () => {
147 const foo = ref(1);
148 const bar = ref(1);
149
150 const { executeQuery, query$ } = createQuery({
151 query: ref('{ test }'),
152 variables: ref({
153 prop: foo,
154 nested: {
155 prop: bar,
156 },
157 }),
158 });
159
160 await query$;
161 expect(executeQuery).toHaveBeenCalledTimes(1);
162 expect(query$.operation.value).toHaveProperty('variables.prop', 1);
163
164 foo.value++;
165 await query$;
166 expect(executeQuery).toHaveBeenCalledTimes(2);
167 expect(query$.operation.value).toHaveProperty('variables.prop', 2);
168
169 bar.value++;
170 await query$;
171 expect(executeQuery).toHaveBeenCalledTimes(3);
172 expect(query$.operation.value).toHaveProperty('variables.nested.prop', 2);
173 });
174
175 it('reacts to getter variables changing', async () => {
176 const foo = ref(1);
177 const bar = ref(1);
178
179 const { executeQuery, query$ } = createQuery({
180 query: ref('{ test }'),
181 variables: () => ({
182 prop: foo.value,
183 nested: {
184 prop: bar.value,
185 },
186 }),
187 });
188
189 await query$;
190 expect(executeQuery).toHaveBeenCalledTimes(1);
191 expect(query$.operation.value).toHaveProperty('variables.prop', 1);
192
193 foo.value++;
194 await query$;
195 expect(executeQuery).toHaveBeenCalledTimes(2);
196 expect(query$.operation.value).toHaveProperty('variables.prop', 2);
197
198 bar.value++;
199 await query$;
200 expect(executeQuery).toHaveBeenCalledTimes(3);
201 expect(query$.operation.value).toHaveProperty('variables.nested.prop', 2);
202 });
203
204 it('reacts to reactive variables changing', async () => {
205 const prop = ref(1);
206 const variables = reactive({ prop: 1, deep: { nested: { prop } } });
207
208 const { executeQuery, query$ } = createQuery({
209 query: ref('{ test }'),
210 variables,
211 });
212
213 await query$;
214 expect(executeQuery).toHaveBeenCalledTimes(1);
215 expect(query$.operation.value).toHaveProperty('variables.prop', 1);
216
217 variables.prop++;
218 await query$;
219 expect(executeQuery).toHaveBeenCalledTimes(2);
220 expect(query$.operation.value).toHaveProperty('variables.prop', 2);
221
222 prop.value++;
223 await query$;
224 expect(executeQuery).toHaveBeenCalledTimes(3);
225 expect(query$.operation.value).toHaveProperty(
226 'variables.deep.nested.prop',
227 2
228 );
229 });
230
231 it('reacts to computed variables changing', async () => {
232 const foo = ref(1);
233 const bar = ref(1);
234 const variables = computed(() => ({
235 prop: foo.value,
236 deep: { nested: { prop: bar.value } },
237 }));
238
239 const { executeQuery, query$ } = createQuery({
240 query: ref('{ test }'),
241 variables,
242 });
243
244 await query$;
245 expect(executeQuery).toHaveBeenCalledTimes(1);
246 expect(query$.operation.value).toHaveProperty('variables.prop', 1);
247
248 foo.value++;
249 await query$;
250 expect(executeQuery).toHaveBeenCalledTimes(2);
251 expect(query$.operation.value).toHaveProperty('variables.prop', 2);
252
253 bar.value++;
254 await query$;
255 expect(executeQuery).toHaveBeenCalledTimes(3);
256 expect(query$.operation.value).toHaveProperty(
257 'variables.deep.nested.prop',
258 2
259 );
260 });
261
262 it('reacts to reactive context argument', async () => {
263 const context = ref<{ requestPolicy: RequestPolicy }>({
264 requestPolicy: 'cache-only',
265 });
266
267 const { executeQuery, query$ } = createQuery({
268 query: ref('{ test }'),
269 context,
270 });
271
272 await query$;
273 expect(executeQuery).toHaveBeenCalledTimes(1);
274
275 context.value.requestPolicy = 'network-only';
276 await query$;
277 expect(executeQuery).toHaveBeenCalledTimes(2);
278 });
279
280 it('reacts to callback context argument', async () => {
281 const requestPolicy = ref<RequestPolicy>('cache-only');
282
283 const { executeQuery, query$ } = createQuery({
284 query: ref('{ test }'),
285 context: () => ({
286 requestPolicy: requestPolicy.value,
287 }),
288 });
289
290 await query$;
291 expect(executeQuery).toHaveBeenCalledTimes(1);
292
293 requestPolicy.value = 'network-only';
294 await query$;
295 expect(executeQuery).toHaveBeenCalledTimes(2);
296 });
297
298 it('pauses query when asked to do so', async () => {
299 const subject = makeSubject<any>();
300 const executeQuery = vi
301 .spyOn(client, 'executeQuery')
302 .mockImplementation(
303 () => subject.source as OperationResultSource<OperationResult>
304 );
305
306 const query = useQuery({
307 query: `{ test }`,
308 pause: true,
309 });
310
311 expect(executeQuery).not.toHaveBeenCalled();
312
313 query.resume();
314 await nextTick();
315 expect(query.fetching.value).toBe(true);
316
317 subject.next({ data: { test: true } });
318
319 expect(query.fetching.value).toBe(false);
320 expect(query.data.value).toHaveProperty('test', true);
321 });
322
323 it('pauses query with ref variable', async () => {
324 const pause = ref(true);
325
326 const { executeQuery, query$ } = createQuery({
327 query: ref('{ test }'),
328 pause,
329 });
330
331 await query$;
332 expect(executeQuery).not.toHaveBeenCalled();
333
334 pause.value = false;
335 await query$;
336 expect(executeQuery).toHaveBeenCalledTimes(1);
337
338 query$.pause();
339 query$.resume();
340 await query$;
341 expect(executeQuery).toHaveBeenCalledTimes(2);
342 });
343
344 it('pauses query with computed variable', async () => {
345 const pause = ref(true);
346
347 const { executeQuery, query$ } = createQuery({
348 query: ref('{ test }'),
349 pause: computed(() => pause.value),
350 });
351
352 await query$;
353 expect(executeQuery).not.toHaveBeenCalled();
354
355 pause.value = false;
356 await query$;
357 expect(executeQuery).toHaveBeenCalledTimes(1);
358
359 query$.pause();
360 query$.resume();
361 await query$;
362 // this shouldn't be called, as pause/resume functionality should works in sync with passed `pause` variable, e.g.:
363 // if we pass readonly computed variable, then we want to make sure that its value fully controls the state of the request.
364 expect(executeQuery).toHaveBeenCalledTimes(1);
365 });
366
367 it('pauses query with callback', async () => {
368 const pause = ref(true);
369
370 const { executeQuery, query$ } = createQuery({
371 query: ref('{ test }'),
372 pause: () => pause.value,
373 });
374
375 await query$;
376 expect(executeQuery).not.toHaveBeenCalled();
377
378 pause.value = false;
379 await query$;
380 expect(executeQuery).toHaveBeenCalledTimes(1);
381
382 query$.pause();
383 query$.resume();
384 await query$;
385 // the same as computed variable example - user has full control over the request state if using callback
386 expect(executeQuery).toHaveBeenCalledTimes(1);
387 });
388});