Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 7.8 kB view raw
1import { makeSubject, pipe, map, publish, forEach, Subject } from 'wonka'; 2import { vi, expect, it, beforeEach, afterEach } from 'vitest'; 3 4import { Client } from '../client'; 5import { queryOperation, queryResponse } from '../test-utils'; 6import { ExchangeIO, Operation, OperationResult } from '../types'; 7import { CombinedError, formatDocument } from '../utils'; 8import { ssrExchange } from './ssr'; 9 10let forward: ExchangeIO; 11let exchangeInput; 12let client: Client; 13let input: Subject<Operation>; 14let output; 15 16const serializedQueryResponse = { 17 ...queryResponse, 18 data: JSON.stringify(queryResponse.data), 19}; 20 21beforeEach(() => { 22 input = makeSubject<Operation>(); 23 output = vi.fn(operation => ({ operation })); 24 forward = ops$ => pipe(ops$, map(output)); 25 client = { suspense: true } as any; 26 exchangeInput = { forward, client }; 27}); 28 29afterEach(() => { 30 output.mockClear(); 31}); 32 33it('caches query results correctly', () => { 34 output.mockReturnValueOnce(queryResponse); 35 36 const ssr = ssrExchange(); 37 const { source: ops$, next } = input; 38 const exchange = ssr(exchangeInput)(ops$); 39 40 publish(exchange); 41 next(queryOperation); 42 43 const data = ssr.extractData(); 44 expect(Object.keys(data)).toEqual(['' + queryOperation.key]); 45 46 expect(data).toEqual({ 47 [queryOperation.key]: { 48 data: serializedQueryResponse.data, 49 error: undefined, 50 hasNext: false, 51 }, 52 }); 53}); 54 55it('serializes query results quickly', () => { 56 const result: OperationResult = { 57 ...queryResponse, 58 operation: queryOperation, 59 data: { 60 user: { 61 name: 'Clive', 62 }, 63 }, 64 }; 65 66 const serializedQueryResponse = { 67 ...result, 68 data: JSON.stringify(result.data), 69 }; 70 71 output.mockReturnValueOnce(result); 72 73 const ssr = ssrExchange(); 74 const { source: ops$, next } = input; 75 const exchange = ssr(exchangeInput)(ops$); 76 77 publish(exchange); 78 next(queryOperation); 79 result.data.user.name = 'Not Clive'; 80 81 const data = ssr.extractData(); 82 expect(Object.keys(data)).toEqual(['' + queryOperation.key]); 83 84 expect(data).toEqual({ 85 [queryOperation.key]: { 86 data: serializedQueryResponse.data, 87 error: undefined, 88 hasNext: false, 89 }, 90 }); 91}); 92 93it('caches errored query results correctly', () => { 94 output.mockReturnValueOnce({ 95 ...queryResponse, 96 data: null, 97 error: new CombinedError({ 98 graphQLErrors: ['Oh no!'], 99 }), 100 }); 101 102 const ssr = ssrExchange(); 103 const { source: ops$, next } = input; 104 const exchange = ssr(exchangeInput)(ops$); 105 106 publish(exchange); 107 next(queryOperation); 108 109 const data = ssr.extractData(); 110 expect(Object.keys(data)).toEqual(['' + queryOperation.key]); 111 112 expect(data).toEqual({ 113 [queryOperation.key]: { 114 data: 'null', 115 error: { 116 graphQLErrors: [ 117 { 118 extensions: {}, 119 message: 'Oh no!', 120 path: undefined, 121 }, 122 ], 123 networkError: undefined, 124 }, 125 hasNext: false, 126 }, 127 }); 128}); 129 130it('caches extensions when includeExtensions=true', () => { 131 output.mockReturnValueOnce({ 132 ...queryResponse, 133 extensions: { 134 foo: 'bar', 135 }, 136 }); 137 138 const ssr = ssrExchange({ 139 includeExtensions: true, 140 }); 141 const { source: ops$, next } = input; 142 const exchange = ssr(exchangeInput)(ops$); 143 144 publish(exchange); 145 next(queryOperation); 146 147 const data = ssr.extractData(); 148 expect(Object.keys(data)).toEqual(['' + queryOperation.key]); 149 150 expect(data).toEqual({ 151 [queryOperation.key]: { 152 data: '{"user":{"name":"Clive"}}', 153 extensions: '{"foo":"bar"}', 154 hasNext: false, 155 }, 156 }); 157}); 158 159it('caches complex GraphQLErrors in query results correctly', () => { 160 output.mockReturnValueOnce({ 161 ...queryResponse, 162 data: null, 163 error: new CombinedError({ 164 graphQLErrors: [ 165 { 166 message: 'Oh no!', 167 path: ['Query'], 168 extensions: { test: true }, 169 }, 170 ], 171 }), 172 }); 173 174 const ssr = ssrExchange(); 175 const { source: ops$, next } = input; 176 const exchange = ssr(exchangeInput)(ops$); 177 178 publish(exchange); 179 next(queryOperation); 180 181 const error = ssr.extractData()[queryOperation.key]!.error; 182 183 expect(error).toHaveProperty('graphQLErrors.0.message', 'Oh no!'); 184 expect(error).toHaveProperty('graphQLErrors.0.path', ['Query']); 185 expect(error).toHaveProperty('graphQLErrors.0.extensions.test', true); 186}); 187 188it('resolves cached query results correctly', () => { 189 const onPush = vi.fn(); 190 191 const ssr = ssrExchange({ 192 initialState: { [queryOperation.key]: serializedQueryResponse as any }, 193 }); 194 195 const { source: ops$, next } = input; 196 const exchange = ssr(exchangeInput)(ops$); 197 198 pipe(exchange, forEach(onPush)); 199 next(queryOperation); 200 201 const data = ssr.extractData(); 202 expect(Object.keys(data).length).toBe(1); 203 expect(output).not.toHaveBeenCalled(); 204 expect(onPush).toHaveBeenCalledWith({ 205 ...queryResponse, 206 stale: false, 207 hasNext: false, 208 operation: { 209 ...queryResponse.operation, 210 context: { 211 ...queryResponse.operation.context, 212 meta: { 213 cacheOutcome: 'hit', 214 }, 215 }, 216 }, 217 }); 218}); 219 220it('resolves deferred, cached query results correctly', () => { 221 const onPush = vi.fn(); 222 223 const ssr = ssrExchange({ 224 isClient: true, 225 initialState: { 226 [queryOperation.key]: { 227 ...(serializedQueryResponse as any), 228 hasNext: true, 229 }, 230 }, 231 }); 232 233 const { source: ops$, next } = input; 234 const exchange = ssr(exchangeInput)(ops$); 235 236 pipe(exchange, forEach(onPush)); 237 next(queryOperation); 238 239 const data = ssr.extractData(); 240 expect(Object.keys(data).length).toBe(1); 241 expect(output).toHaveBeenCalledTimes(1); 242 expect(onPush).toHaveBeenCalledTimes(2); 243 expect(onPush.mock.calls[1][0]).toEqual({ 244 ...queryResponse, 245 hasNext: true, 246 stale: false, 247 operation: { 248 ...queryResponse.operation, 249 context: { 250 ...queryResponse.operation.context, 251 meta: { 252 cacheOutcome: 'hit', 253 }, 254 }, 255 }, 256 }); 257 258 expect(output.mock.calls[0][0].query).toBe( 259 formatDocument(queryOperation.query) 260 ); 261}); 262 263it('deletes cached results in non-suspense environments', async () => { 264 client.suspense = false; 265 const onPush = vi.fn(); 266 const ssr = ssrExchange(); 267 268 ssr.restoreData({ [queryOperation.key]: serializedQueryResponse as any }); 269 expect(Object.keys(ssr.extractData()).length).toBe(1); 270 271 const { source: ops$, next } = input; 272 const exchange = ssr(exchangeInput)(ops$); 273 274 pipe(exchange, forEach(onPush)); 275 next(queryOperation); 276 277 await Promise.resolve(); 278 279 expect(Object.keys(ssr.extractData()).length).toBe(0); 280 expect(onPush).toHaveBeenCalledWith({ 281 ...queryResponse, 282 stale: false, 283 hasNext: false, 284 operation: { 285 ...queryResponse.operation, 286 context: { 287 ...queryResponse.operation.context, 288 meta: { 289 cacheOutcome: 'hit', 290 }, 291 }, 292 }, 293 }); 294 295 // NOTE: The operation should not be duplicated 296 expect(output).not.toHaveBeenCalled(); 297}); 298 299it('never allows restoration of invalidated results', async () => { 300 client.suspense = false; 301 302 const onPush = vi.fn(); 303 const initialState = { [queryOperation.key]: serializedQueryResponse as any }; 304 305 const ssr = ssrExchange({ 306 isClient: true, 307 initialState: { ...initialState }, 308 }); 309 310 const { source: ops$, next } = input; 311 const exchange = ssr(exchangeInput)(ops$); 312 313 pipe(exchange, forEach(onPush)); 314 next(queryOperation); 315 316 await Promise.resolve(); 317 318 expect(Object.keys(ssr.extractData()).length).toBe(0); 319 expect(onPush).toHaveBeenCalledTimes(1); 320 expect(output).not.toHaveBeenCalled(); 321 322 ssr.restoreData(initialState); 323 expect(Object.keys(ssr.extractData()).length).toBe(0); 324 325 next(queryOperation); 326 expect(onPush).toHaveBeenCalledTimes(2); 327 expect(output).toHaveBeenCalledTimes(1); 328});