1import {
2 gql,
3 createClient,
4 ExchangeIO,
5 Operation,
6 OperationResult,
7} from '@urql/core';
8import { vi, expect, it, describe, beforeAll } from 'vitest';
9
10import { pipe, share, map, makeSubject, tap, publish } from 'wonka';
11import { queryResponse } from '../../../packages/core/src/test-utils';
12import { offlineExchange } from './offlineExchange';
13
14const mutationOne = gql`
15 mutation {
16 updateAuthor {
17 id
18 name
19 }
20 }
21`;
22
23const mutationOneData = {
24 __typename: 'Mutation',
25 updateAuthor: {
26 __typename: 'Author',
27 id: '123',
28 name: 'Author',
29 },
30};
31
32const queryOne = gql`
33 query {
34 authors {
35 id
36 name
37 __typename
38 }
39 }
40`;
41
42const queryOneData = {
43 __typename: 'Query',
44 authors: [
45 {
46 id: '123',
47 name: 'Me',
48 __typename: 'Author',
49 },
50 ],
51};
52
53const dispatchDebug = vi.fn();
54
55const storage = {
56 onOnline: vi.fn(),
57 writeData: vi.fn(() => Promise.resolve(undefined)),
58 writeMetadata: vi.fn(() => Promise.resolve(undefined)),
59 readData: vi.fn(() => Promise.resolve({})),
60 readMetadata: vi.fn(() => Promise.resolve([])),
61};
62
63describe('storage', () => {
64 it('should read the metadata and dispatch operations on initialization', () => {
65 const client = createClient({
66 url: 'http://0.0.0.0',
67 exchanges: [],
68 });
69
70 const reexecuteOperation = vi
71 .spyOn(client, 'reexecuteOperation')
72 .mockImplementation(() => undefined);
73 const op = client.createRequestOperation('mutation', {
74 key: 1,
75 query: mutationOne,
76 variables: {},
77 });
78
79 const response = vi.fn((forwardOp: Operation): OperationResult => {
80 expect(forwardOp.key).toBe(op.key);
81 return {
82 ...queryResponse,
83 operation: forwardOp,
84 data: mutationOneData,
85 };
86 });
87
88 const { source: ops$ } = makeSubject<Operation>();
89 const result = vi.fn();
90 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
91
92 vi.useFakeTimers();
93 pipe(
94 offlineExchange({ storage })({ forward, client, dispatchDebug })(ops$),
95 tap(result),
96 publish
97 );
98 vi.runAllTimers();
99
100 expect(storage.readMetadata).toBeCalledTimes(1);
101 expect(reexecuteOperation).toBeCalledTimes(0);
102 });
103});
104
105describe('offline', () => {
106 beforeAll(() => {
107 vi.resetAllMocks();
108 globalThis.navigator = { onLine: true } as any;
109 });
110
111 it('should intercept errored mutations', () => {
112 const onlineSpy = vi.spyOn(globalThis.navigator, 'onLine', 'get');
113
114 const client = createClient({
115 url: 'http://0.0.0.0',
116 exchanges: [],
117 });
118
119 const queryOp = client.createRequestOperation('query', {
120 key: 1,
121 query: queryOne,
122 variables: {},
123 });
124
125 const mutationOp = client.createRequestOperation('mutation', {
126 key: 2,
127 query: mutationOne,
128 variables: {},
129 });
130
131 const response = vi.fn((forwardOp: Operation): OperationResult => {
132 if (forwardOp.key === queryOp.key) {
133 onlineSpy.mockReturnValueOnce(true);
134 return { ...queryResponse, operation: forwardOp, data: queryOneData };
135 } else {
136 onlineSpy.mockReturnValueOnce(false);
137 return {
138 ...queryResponse,
139 operation: forwardOp,
140 // @ts-ignore
141 error: { networkError: new Error('failed to fetch') },
142 };
143 }
144 });
145
146 const { source: ops$, next } = makeSubject<Operation>();
147 const result = vi.fn();
148 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
149
150 pipe(
151 offlineExchange({
152 storage,
153 optimistic: {
154 updateAuthor: () => ({
155 id: '123',
156 name: 'URQL',
157 __typename: 'Author',
158 }),
159 },
160 })({ forward, client, dispatchDebug })(ops$),
161 tap(result),
162 publish
163 );
164
165 next(queryOp);
166 expect(result).toBeCalledTimes(1);
167 expect(queryOneData).toMatchObject(result.mock.calls[0][0].data);
168
169 next(mutationOp);
170 expect(result).toBeCalledTimes(2);
171
172 next(queryOp);
173 expect(result).toBeCalledTimes(3);
174 });
175
176 it('should intercept errored queries', async () => {
177 const client = createClient({
178 url: 'http://0.0.0.0',
179 exchanges: [],
180 });
181 const onlineSpy = vi
182 .spyOn(navigator, 'onLine', 'get')
183 .mockReturnValueOnce(false);
184
185 const queryOp = client.createRequestOperation('query', {
186 key: 1,
187 query: queryOne,
188 variables: undefined,
189 });
190
191 const response = vi.fn((forwardOp: Operation): OperationResult => {
192 onlineSpy.mockReturnValueOnce(false);
193 return {
194 operation: forwardOp,
195 // @ts-ignore
196 error: { networkError: new Error('failed to fetch') },
197 };
198 });
199
200 const { source: ops$, next } = makeSubject<Operation>();
201 const result = vi.fn();
202 const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
203
204 pipe(
205 offlineExchange({ storage })({ forward, client, dispatchDebug })(ops$),
206 tap(result),
207 publish
208 );
209
210 next(queryOp);
211
212 expect(result).toBeCalledTimes(1);
213 expect(response).toBeCalledTimes(1);
214
215 expect(result.mock.calls[0][0]).toEqual({
216 data: null,
217 error: undefined,
218 extensions: undefined,
219 operation: expect.any(Object),
220 hasNext: false,
221 stale: false,
222 });
223
224 expect(result.mock.calls[0][0]).toHaveProperty(
225 'operation.context.meta.cacheOutcome',
226 'miss'
227 );
228 });
229
230 it('should flush the queue when we become online', async () => {
231 let resolveOnOnlineCalled: () => void;
232 const onOnlineCalled = new Promise<void>(
233 resolve => (resolveOnOnlineCalled = resolve)
234 );
235
236 let flush: () => {};
237 storage.onOnline.mockImplementation(cb => {
238 flush = cb;
239 resolveOnOnlineCalled!();
240 });
241
242 const onlineSpy = vi.spyOn(navigator, 'onLine', 'get');
243
244 const client = createClient({
245 url: 'http://0.0.0.0',
246 exchanges: [],
247 });
248
249 const mutationOp = client.createRequestOperation('mutation', {
250 key: 1,
251 query: mutationOne,
252 variables: {},
253 });
254
255 const response = vi.fn((forwardOp: Operation): OperationResult => {
256 onlineSpy.mockReturnValueOnce(false);
257 return {
258 operation: forwardOp,
259 // @ts-ignore
260 error: { networkError: new Error('failed to fetch') },
261 };
262 });
263
264 const { source: ops$, next } = makeSubject<Operation>();
265 const result = vi.fn();
266 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
267
268 pipe(
269 offlineExchange({
270 storage,
271 optimistic: {
272 updateAuthor: () => ({
273 id: '123',
274 name: 'URQL',
275 __typename: 'Author',
276 }),
277 },
278 })({ forward, client, dispatchDebug })(ops$),
279 tap(result),
280 publish
281 );
282
283 next(mutationOp);
284
285 await onOnlineCalled;
286
287 flush!();
288 });
289});