1// @vitest-environment jsdom
2
3import { renderHook, testEffect } from '@solidjs/testing-library';
4import {
5 OperationResult,
6 OperationResultSource,
7 createClient,
8 createRequest,
9 gql,
10} from '@urql/core';
11import { expect, it, describe, vi } from 'vitest';
12import { makeSubject } from 'wonka';
13import {
14 CreateSubscriptionState,
15 createSubscription,
16} from './createSubscription';
17import { createEffect, createSignal } from 'solid-js';
18
19const QUERY = gql`
20 subscription {
21 value
22 }
23`;
24
25const client = createClient({
26 url: '/graphql',
27 exchanges: [],
28 suspense: false,
29});
30
31vi.mock('./context', () => {
32 const useClient = () => {
33 return client!;
34 };
35
36 return { useClient };
37});
38
39// Given that it is not possible to directly listen to all store changes it is necessary
40// to access all relevant parts on which `createEffect` should listen on
41const markStateDependencies = (state: CreateSubscriptionState<any, any>) => {
42 state.data;
43 state.error;
44 state.extensions;
45 state.fetching;
46 state.operation;
47 state.stale;
48};
49
50describe('createSubscription', () => {
51 it('should receive data', () => {
52 return testEffect(done => {
53 const subject =
54 makeSubject<Pick<OperationResult<{ value: number }, any>, 'data'>>();
55 const executeQuery = vi
56 .spyOn(client, 'executeSubscription')
57 .mockImplementation(
58 () => subject.source as OperationResultSource<OperationResult>
59 );
60
61 const request = createRequest(QUERY, undefined);
62 const operation = client.createRequestOperation('subscription', request);
63
64 const [state] = createSubscription<
65 { value: number },
66 { value: number },
67 { variable: number }
68 >({
69 query: QUERY,
70 });
71
72 createEffect((run: number = 0) => {
73 markStateDependencies(state);
74
75 switch (run) {
76 case 0: {
77 expect(state).toMatchObject({
78 data: undefined,
79 stale: false,
80 operation: operation,
81 error: undefined,
82 extensions: undefined,
83 fetching: true,
84 });
85 expect(executeQuery).toEqual(expect.any(Function));
86 expect(executeQuery).toHaveBeenCalledOnce();
87 expect(client.executeSubscription).toBeCalledWith(
88 {
89 key: expect.any(Number),
90 query: expect.any(Object),
91 variables: {},
92 },
93 undefined
94 );
95 subject.next({ data: { value: 0 } });
96 break;
97 }
98 case 1: {
99 expect(state.data).toEqual({ value: 0 });
100 subject.next({ data: { value: 1 } });
101 break;
102 }
103 case 2: {
104 expect(state.data).toEqual({ value: 1 });
105 // expect(state.fetching).toEqual(true);
106 subject.complete();
107 break;
108 }
109 case 3: {
110 expect(state.fetching).toEqual(false);
111 expect(state.data).toEqual({ value: 1 });
112 done();
113 }
114 }
115
116 return run + 1;
117 });
118 });
119 });
120
121 it('should call handler', () => {
122 const handler = vi.fn();
123 const subject =
124 makeSubject<Pick<OperationResult<{ value: number }, any>, 'data'>>();
125 vi.spyOn(client, 'executeSubscription').mockImplementation(
126 () => subject.source as OperationResultSource<OperationResult>
127 );
128
129 return testEffect(done => {
130 const [state] = createSubscription<
131 { value: number },
132 { value: number },
133 { variable: number }
134 >(
135 {
136 query: QUERY,
137 },
138 handler
139 );
140
141 createEffect((run: number = 0) => {
142 markStateDependencies(state);
143 switch (run) {
144 case 0: {
145 expect(state.fetching).toEqual(true);
146 subject.next({ data: { value: 0 } });
147
148 break;
149 }
150 case 1: {
151 expect(handler).toHaveBeenCalledOnce();
152 expect(handler).toBeCalledWith(undefined, { value: 0 });
153 done();
154 break;
155 }
156 }
157
158 return run + 1;
159 });
160 });
161 });
162
163 it('should unsubscribe on teardown', () => {
164 const subject =
165 makeSubject<Pick<OperationResult<{ value: number }, any>, 'data'>>();
166 vi.spyOn(client, 'executeSubscription').mockImplementation(
167 () => subject.source as OperationResultSource<OperationResult>
168 );
169
170 const {
171 result: [state],
172 cleanup,
173 } = renderHook(() =>
174 createSubscription<{ value: number }, { variable: number }>({
175 query: QUERY,
176 })
177 );
178
179 return testEffect(done =>
180 createEffect((run: number = 0) => {
181 if (run === 0) {
182 expect(state.fetching).toEqual(true);
183 cleanup();
184 }
185
186 if (run === 1) {
187 expect(state.fetching).toEqual(false);
188 done();
189 }
190
191 return run + 1;
192 })
193 );
194 });
195
196 it('should skip executing query when paused', () => {
197 const subject =
198 makeSubject<Pick<OperationResult<{ value: number }, any>, 'data'>>();
199 vi.spyOn(client, 'executeSubscription').mockImplementation(
200 () => subject.source as OperationResultSource<OperationResult>
201 );
202
203 return testEffect(done => {
204 const [pause, setPause] = createSignal<boolean>(true);
205
206 const [state] = createSubscription<
207 { value: number },
208 { value: number },
209 { variable: number }
210 >({ query: QUERY, pause: pause });
211
212 createEffect((run: number = 0) => {
213 switch (run) {
214 case 0: {
215 expect(state.fetching).toBe(false);
216 setPause(false);
217 break;
218 }
219 case 1: {
220 expect(state.fetching).toBe(true);
221 expect(state.data).toBeUndefined();
222 subject.next({ data: { value: 1 } });
223
224 break;
225 }
226 case 2: {
227 expect(state.data).toStrictEqual({ value: 1 });
228 done();
229 break;
230 }
231 }
232
233 return run + 1;
234 });
235 });
236 });
237
238 it('should override pause when execute executeSubscription', () => {
239 const subject =
240 makeSubject<Pick<OperationResult<{ value: number }, any>, 'data'>>();
241 const executeQuery = vi
242 .spyOn(client, 'executeSubscription')
243 .mockImplementation(
244 () => subject.source as OperationResultSource<OperationResult>
245 );
246
247 return testEffect(done => {
248 const [state, executeSubscription] = createSubscription<
249 { value: number },
250 { value: number },
251 { variable: number }
252 >({
253 query: QUERY,
254 pause: true,
255 });
256
257 createEffect((run: number = 0) => {
258 markStateDependencies(state);
259
260 switch (run) {
261 case 0: {
262 expect(state.fetching).toEqual(false);
263 expect(executeQuery).not.toBeCalled();
264
265 executeSubscription();
266
267 break;
268 }
269 case 1: {
270 expect(state.fetching).toEqual(true);
271 expect(executeQuery).toHaveBeenCalledOnce();
272 subject.next({ data: { value: 1 } });
273 break;
274 }
275 case 2: {
276 expect(state.data).toStrictEqual({ value: 1 });
277 done();
278 break;
279 }
280 }
281
282 return run + 1;
283 });
284 });
285 });
286
287 it('should aggregate results', () => {
288 const subject =
289 makeSubject<Pick<OperationResult<{ value: number }, any>, 'data'>>();
290 vi.spyOn(client, 'executeSubscription').mockImplementation(
291 () => subject.source as OperationResultSource<OperationResult>
292 );
293
294 return testEffect(done => {
295 const [state] = createSubscription<
296 { value: number },
297 { merged: number },
298 { variable: number }
299 >(
300 {
301 query: QUERY,
302 },
303 (prev, next) => {
304 if (prev === undefined) {
305 return {
306 merged: 0 + next.value,
307 };
308 }
309
310 return { merged: prev.merged + next.value };
311 }
312 );
313
314 createEffect((run: number = 0) => {
315 markStateDependencies(state);
316 switch (run) {
317 case 0: {
318 expect(state.fetching).toEqual(true);
319 subject.next({ data: { value: 1 } });
320
321 break;
322 }
323 case 1: {
324 expect(state.data).toEqual({ merged: 1 });
325 subject.next({ data: { value: 2 } });
326
327 break;
328 }
329 case 2: {
330 expect(state.data).toEqual({ merged: 3 });
331
332 done();
333 break;
334 }
335 }
336
337 return run + 1;
338 });
339 });
340 });
341});