Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 5.9 kB view raw
1import { 2 makeSubject, 3 map, 4 pipe, 5 publish, 6 Source, 7 Subject, 8 forEach, 9 scan, 10 toPromise, 11} from 'wonka'; 12import { vi, expect, it, beforeEach, describe } from 'vitest'; 13 14import { Client } from '../client'; 15import { 16 mutationOperation, 17 mutationResponse, 18 queryOperation, 19 queryResponse, 20 subscriptionOperation, 21 subscriptionResult, 22 undefinedQueryResponse, 23} from '../test-utils'; 24import { Operation, OperationResult, ExchangeInput } from '../types'; 25import { cacheExchange } from './cache'; 26 27const reexecuteOperation = vi.fn(); 28const dispatchDebug = vi.fn(); 29 30let response; 31let exchangeArgs: ExchangeInput; 32let forwardedOperations: Operation[]; 33let input: Subject<Operation>; 34 35beforeEach(() => { 36 response = queryResponse; 37 forwardedOperations = []; 38 input = makeSubject<Operation>(); 39 40 // Collect all forwarded operations 41 const forward = (s: Source<Operation>) => { 42 return pipe( 43 s, 44 map(op => { 45 forwardedOperations.push(op); 46 return response; 47 }) 48 ); 49 }; 50 51 const client = { 52 reexecuteOperation: reexecuteOperation as any, 53 } as Client; 54 55 exchangeArgs = { forward, client, dispatchDebug }; 56}); 57 58describe('on query', () => { 59 it('forwards to next exchange when no cache hit', () => { 60 const { source: ops$, next, complete } = input; 61 const exchange = cacheExchange(exchangeArgs)(ops$); 62 63 publish(exchange); 64 next(queryOperation); 65 complete(); 66 expect(forwardedOperations.length).toBe(1); 67 expect(reexecuteOperation).not.toBeCalled(); 68 }); 69 70 it('caches results', () => { 71 const { source: ops$, next, complete } = input; 72 const exchange = cacheExchange(exchangeArgs)(ops$); 73 74 publish(exchange); 75 next(queryOperation); 76 next(queryOperation); 77 complete(); 78 expect(forwardedOperations.length).toBe(1); 79 expect(reexecuteOperation).not.toBeCalled(); 80 }); 81 82 it('respects cache-and-network', () => { 83 const { source: ops$, next, complete } = input; 84 const result = vi.fn(); 85 const exchange = cacheExchange(exchangeArgs)(ops$); 86 87 pipe(exchange, forEach(result)); 88 next(queryOperation); 89 90 next({ 91 ...queryOperation, 92 context: { 93 ...queryOperation.context, 94 requestPolicy: 'cache-and-network', 95 }, 96 }); 97 98 complete(); 99 expect(forwardedOperations.length).toBe(1); 100 expect(reexecuteOperation).toHaveBeenCalledTimes(1); 101 expect(result).toHaveBeenCalledTimes(2); 102 expect(result.mock.calls[1][0].stale).toBe(true); 103 104 expect(reexecuteOperation.mock.calls[0][0]).toEqual({ 105 ...queryOperation, 106 context: { ...queryOperation.context, requestPolicy: 'network-only' }, 107 }); 108 }); 109 110 it('respects cache-only', () => { 111 const { source: ops$, next, complete } = input; 112 const exchange = cacheExchange(exchangeArgs)(ops$); 113 114 publish(exchange); 115 next({ 116 ...queryOperation, 117 context: { 118 ...queryOperation.context, 119 requestPolicy: 'cache-only', 120 }, 121 }); 122 complete(); 123 expect(forwardedOperations.length).toBe(0); 124 expect(reexecuteOperation).not.toBeCalled(); 125 }); 126 127 describe('cache hit', () => { 128 it('is miss when operation is forwarded', () => { 129 const { source: ops$, next, complete } = input; 130 const exchange = cacheExchange(exchangeArgs)(ops$); 131 132 publish(exchange); 133 next(queryOperation); 134 complete(); 135 136 expect(forwardedOperations[0].context).toHaveProperty( 137 'meta.cacheOutcome', 138 'miss' 139 ); 140 }); 141 142 it('is true when cached response is returned', async () => { 143 const { source: ops$, next, complete } = input; 144 const exchange = cacheExchange(exchangeArgs)(ops$); 145 146 const results$ = pipe( 147 exchange, 148 scan((acc, x) => [...acc, x], [] as OperationResult[]), 149 toPromise 150 ); 151 152 publish(exchange); 153 next(queryOperation); 154 next(queryOperation); 155 complete(); 156 157 const results = await results$; 158 expect(results[1].operation.context).toHaveProperty( 159 'meta.cacheOutcome', 160 'hit' 161 ); 162 }); 163 }); 164}); 165 166describe('on mutation', () => { 167 it('does not cache', () => { 168 response = mutationResponse; 169 const { source: ops$, next, complete } = input; 170 const exchange = cacheExchange(exchangeArgs)(ops$); 171 172 publish(exchange); 173 next(mutationOperation); 174 next(mutationOperation); 175 complete(); 176 expect(forwardedOperations.length).toBe(2); 177 expect(reexecuteOperation).not.toBeCalled(); 178 }); 179}); 180 181describe('on subscription', () => { 182 it('forwards subscriptions', () => { 183 response = subscriptionResult; 184 const { source: ops$, next, complete } = input; 185 const exchange = cacheExchange(exchangeArgs)(ops$); 186 187 publish(exchange); 188 next(subscriptionOperation); 189 next(subscriptionOperation); 190 complete(); 191 expect(forwardedOperations.length).toBe(2); 192 expect(reexecuteOperation).not.toBeCalled(); 193 }); 194}); 195 196// Empty query response implies the data propertys is undefined 197describe('on empty query response', () => { 198 beforeEach(() => { 199 response = undefinedQueryResponse; 200 forwardedOperations = []; 201 input = makeSubject<Operation>(); 202 203 // Collect all forwarded operations 204 const forward = (s: Source<Operation>) => { 205 return pipe( 206 s, 207 map(op => { 208 forwardedOperations.push(op); 209 return response; 210 }) 211 ); 212 }; 213 214 const client = { 215 reexecuteOperation: reexecuteOperation as any, 216 } as Client; 217 218 exchangeArgs = { forward, client, dispatchDebug }; 219 }); 220 221 it('does not cache response', () => { 222 const { source: ops$, next, complete } = input; 223 const exchange = cacheExchange(exchangeArgs)(ops$); 224 225 publish(exchange); 226 next(queryOperation); 227 next(queryOperation); 228 complete(); 229 // 2 indicates it's not cached. 230 expect(forwardedOperations.length).toBe(2); 231 expect(reexecuteOperation).not.toBeCalled(); 232 }); 233});