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});