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