1import { empty, fromValue, pipe, Source, subscribe, toPromise } from 'wonka';
2import {
3 vi,
4 expect,
5 it,
6 beforeEach,
7 describe,
8 beforeAll,
9 Mock,
10 afterEach,
11 afterAll,
12} from 'vitest';
13
14import { Client } from '../client';
15import { makeOperation } from '../utils';
16import { queryOperation } from '../test-utils';
17import { OperationResult } from '../types';
18import { fetchExchange } from './fetch';
19
20const fetch = (globalThis as any).fetch as Mock;
21const abort = vi.fn();
22
23const abortError = new Error();
24abortError.name = 'AbortError';
25
26beforeAll(() => {
27 (globalThis as any).AbortController = function AbortController() {
28 this.signal = undefined;
29 this.abort = abort;
30 };
31});
32
33afterEach(() => {
34 fetch.mockClear();
35 abort.mockClear();
36});
37
38afterAll(() => {
39 (globalThis as any).AbortController = undefined;
40});
41
42const response = JSON.stringify({
43 status: 200,
44 data: {
45 data: {
46 user: 1200,
47 },
48 },
49});
50
51const exchangeArgs = {
52 dispatchDebug: vi.fn(),
53 forward: () => empty as Source<OperationResult>,
54 client: {
55 debugTarget: {
56 dispatchEvent: vi.fn(),
57 },
58 } as any as Client,
59};
60
61describe('on success', () => {
62 beforeEach(() => {
63 fetch.mockResolvedValue({
64 status: 200,
65 headers: { get: () => 'application/json' },
66 text: vi.fn().mockResolvedValue(response),
67 });
68 });
69
70 it('returns response data', async () => {
71 const fetchOptions = vi.fn().mockReturnValue({});
72
73 const data = await pipe(
74 fromValue({
75 ...queryOperation,
76 context: {
77 ...queryOperation.context,
78 fetchOptions,
79 },
80 }),
81 fetchExchange(exchangeArgs),
82 toPromise
83 );
84
85 expect(data).toMatchSnapshot();
86 expect(fetchOptions).toHaveBeenCalled();
87 expect(fetch.mock.calls[0][1].body).toMatchSnapshot();
88 });
89});
90
91describe('on error', () => {
92 beforeEach(() => {
93 fetch.mockResolvedValue({
94 status: 400,
95 headers: { get: () => 'application/json' },
96 text: vi.fn().mockResolvedValue(JSON.stringify({})),
97 });
98 });
99
100 it('returns error data', async () => {
101 const data = await pipe(
102 fromValue(queryOperation),
103 fetchExchange(exchangeArgs),
104 toPromise
105 );
106
107 expect(data).toMatchSnapshot();
108 });
109
110 it('returns error data with status 400 and manual redirect mode', async () => {
111 const fetchOptions = vi.fn().mockReturnValue({ redirect: 'manual' });
112
113 const data = await pipe(
114 fromValue({
115 ...queryOperation,
116 context: {
117 ...queryOperation.context,
118 fetchOptions,
119 },
120 }),
121 fetchExchange(exchangeArgs),
122 toPromise
123 );
124
125 expect(data).toMatchSnapshot();
126 });
127
128 it('ignores the error when a result is available', async () => {
129 fetch.mockResolvedValue({
130 status: 400,
131 headers: { get: () => 'application/json' },
132 text: vi.fn().mockResolvedValue(response),
133 });
134
135 const data = await pipe(
136 fromValue(queryOperation),
137 fetchExchange(exchangeArgs),
138 toPromise
139 );
140
141 expect(data.data).toEqual(JSON.parse(response).data);
142 });
143});
144
145describe('on teardown', () => {
146 const fail = () => {
147 expect(true).toEqual(false);
148 };
149
150 it('does not start the outgoing request on immediate teardowns', async () => {
151 fetch.mockImplementation(async () => {
152 await new Promise(() => {
153 /*noop*/
154 });
155 });
156
157 const { unsubscribe } = pipe(
158 fromValue(queryOperation),
159 fetchExchange(exchangeArgs),
160 subscribe(fail)
161 );
162
163 unsubscribe();
164
165 // NOTE: We can only observe the async iterator's final run after a macro tick
166 await new Promise(resolve => setTimeout(resolve));
167 expect(fetch).toHaveBeenCalledTimes(0);
168 expect(abort).toHaveBeenCalledTimes(1);
169 });
170
171 it('aborts the outgoing request', async () => {
172 fetch.mockResolvedValue({
173 status: 200,
174 headers: new Map([['Content-Type', 'application/json']]),
175 text: vi.fn().mockResolvedValue('{ "data": null }'),
176 });
177
178 const { unsubscribe } = pipe(
179 fromValue(queryOperation),
180 fetchExchange(exchangeArgs),
181 subscribe(() => {
182 /*noop*/
183 })
184 );
185
186 await new Promise(resolve => setTimeout(resolve));
187 unsubscribe();
188
189 // NOTE: We can only observe the async iterator's final run after a macro tick
190 await new Promise(resolve => setTimeout(resolve));
191 expect(fetch).toHaveBeenCalledTimes(1);
192 expect(abort).toHaveBeenCalledTimes(1);
193 });
194
195 it('does not call the query', () => {
196 fetch.mockResolvedValue(new Response('text', { status: 200 }));
197
198 pipe(
199 fromValue(
200 makeOperation('teardown', queryOperation, queryOperation.context)
201 ),
202 fetchExchange(exchangeArgs),
203 subscribe(fail)
204 );
205
206 expect(fetch).toHaveBeenCalledTimes(0);
207 expect(abort).toHaveBeenCalledTimes(0);
208 });
209});