Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 6.9 kB view raw
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});