272 lines
7.8 KiB
Plaintext
272 lines
7.8 KiB
Plaintext
import crossFetch from 'cross-fetch';
|
|
|
|
// Modified from https://github.com/jimmywarting/node-domexception,
|
|
// which is commonjs only.
|
|
if (!globalThis.DOMException) {
|
|
try {
|
|
const { MessageChannel } = await import('worker_threads'),
|
|
port = new MessageChannel().port1,
|
|
ab = new ArrayBuffer();
|
|
port.postMessage(ab, [ab, ab]);
|
|
} catch (err) {
|
|
err.constructor.name === 'DOMException' && (globalThis.DOMException = err.constructor);
|
|
}
|
|
}
|
|
|
|
export default function createFetchMocker(vi) {
|
|
globalThis.fetch = crossFetch;
|
|
globalThis.Response = crossFetch.Response;
|
|
globalThis.Headers = crossFetch.Headers;
|
|
globalThis.Request = crossFetch.Request;
|
|
|
|
const ActualResponse = Response;
|
|
|
|
function responseWrapper(body, init) {
|
|
if (body && typeof body.constructor === 'function' && body.constructor.__isFallback) {
|
|
const response = new ActualResponse(null, init);
|
|
response.body = body;
|
|
|
|
const actualClone = response.clone;
|
|
response.clone = () => {
|
|
const clone = actualClone.call(response);
|
|
const [body1, body2] = body.tee();
|
|
response.body = body1;
|
|
clone.body = body2;
|
|
return clone;
|
|
};
|
|
|
|
return response;
|
|
}
|
|
|
|
return new ActualResponse(body, init);
|
|
}
|
|
|
|
function responseInit(resp, init) {
|
|
if (typeof resp.init === 'object') {
|
|
return resp.init;
|
|
} else {
|
|
init = Object.assign({}, init || {});
|
|
for (const field of ['status', 'statusText', 'headers', 'url']) {
|
|
if (field in resp) {
|
|
init[field] = resp[field];
|
|
}
|
|
}
|
|
return init;
|
|
}
|
|
}
|
|
|
|
function requestMatches(urlOrPredicate) {
|
|
const predicate =
|
|
urlOrPredicate instanceof RegExp
|
|
? (input) => urlOrPredicate.test(input.url)
|
|
: typeof urlOrPredicate === 'string'
|
|
? (input) => input.url === urlOrPredicate
|
|
: urlOrPredicate;
|
|
return (input, reqInit) => {
|
|
const req = normalizeRequest(input, reqInit);
|
|
return [predicate(req), req];
|
|
};
|
|
}
|
|
|
|
function requestNotMatches(urlOrPredicate) {
|
|
const matches = requestMatches(urlOrPredicate);
|
|
return (input) => {
|
|
const result = matches(input);
|
|
return [!result[0], result[1]];
|
|
};
|
|
}
|
|
|
|
function staticMatches(value) {
|
|
return (input, reqInit) => {
|
|
return [value, normalizeRequest(input, reqInit)];
|
|
};
|
|
}
|
|
|
|
const isFn = (unknown) => typeof unknown === 'function';
|
|
|
|
const isMocking = vi.fn(staticMatches(true));
|
|
|
|
const abortError = () => new DOMException('The operation was aborted. ', 'AbortError');
|
|
|
|
const abort = () => {
|
|
throw abortError();
|
|
};
|
|
|
|
const abortAsync = () => {
|
|
return Promise.reject(abortError());
|
|
};
|
|
|
|
const toPromise = (val) => (val instanceof Promise ? val : Promise.resolve(val));
|
|
|
|
const normalizeResponse = (bodyOrFunction, init) => (input, reqInit) => {
|
|
const [mocked, request] = isMocking(input, reqInit);
|
|
return mocked
|
|
? isFn(bodyOrFunction)
|
|
? toPromise(bodyOrFunction(request)).then((resp) => {
|
|
if (request.signal && request.signal.aborted) {
|
|
abort();
|
|
}
|
|
return typeof resp === 'string'
|
|
? responseWrapper(resp, init)
|
|
: responseWrapper(resp.body, responseInit(resp, init));
|
|
})
|
|
: new Promise((resolve, reject) => {
|
|
if (request.signal && request.signal.aborted) {
|
|
reject(abortError());
|
|
return;
|
|
}
|
|
resolve(responseWrapper(bodyOrFunction, init));
|
|
})
|
|
: crossFetch.fetch(input, reqInit);
|
|
};
|
|
|
|
const normalizeRequest = (input, reqInit) => {
|
|
if (input instanceof Request) {
|
|
if (input.signal && input.signal.aborted) {
|
|
abort();
|
|
}
|
|
return input;
|
|
} else if (typeof input === 'string') {
|
|
if (reqInit && reqInit.signal && reqInit.signal.aborted) {
|
|
abort();
|
|
}
|
|
return new Request(input, reqInit);
|
|
} else if (typeof input.toString === 'function') {
|
|
if (reqInit && reqInit.signal && reqInit.signal.aborted) {
|
|
abort();
|
|
}
|
|
return new Request(input.toString(), reqInit);
|
|
} else {
|
|
throw new TypeError('Unable to parse input as string or Request');
|
|
}
|
|
};
|
|
|
|
const normalizeError = (errorOrFunction) =>
|
|
isFn(errorOrFunction) ? errorOrFunction : () => Promise.reject(errorOrFunction);
|
|
|
|
const fetch = vi.fn(normalizeResponse(''));
|
|
fetch.Headers = Headers;
|
|
fetch.Response = responseWrapper;
|
|
fetch.Request = Request;
|
|
fetch.mockResponse = (bodyOrFunction, init) => fetch.mockImplementation(normalizeResponse(bodyOrFunction, init));
|
|
|
|
fetch.mockReject = (errorOrFunction) => fetch.mockImplementation(normalizeError(errorOrFunction));
|
|
|
|
fetch.mockAbort = () => fetch.mockImplementation(abortAsync);
|
|
fetch.mockAbortOnce = () => fetch.mockImplementationOnce(abortAsync);
|
|
|
|
const mockResponseOnce = (bodyOrFunction, init) =>
|
|
fetch.mockImplementationOnce(normalizeResponse(bodyOrFunction, init));
|
|
|
|
fetch.mockResponseOnce = mockResponseOnce;
|
|
|
|
fetch.once = mockResponseOnce;
|
|
|
|
fetch.mockRejectOnce = (errorOrFunction) => fetch.mockImplementationOnce(normalizeError(errorOrFunction));
|
|
|
|
fetch.mockResponses = (...responses) => {
|
|
responses.forEach((response) => {
|
|
if (Array.isArray(response)) {
|
|
const [body, init] = response;
|
|
fetch.mockImplementationOnce(normalizeResponse(body, init));
|
|
} else {
|
|
fetch.mockImplementationOnce(normalizeResponse(response));
|
|
}
|
|
});
|
|
return fetch;
|
|
};
|
|
|
|
fetch.isMocking = (req, reqInit) => isMocking(req, reqInit)[0];
|
|
|
|
fetch.mockIf = fetch.doMockIf = (urlOrPredicate, bodyOrFunction, init) => {
|
|
isMocking.mockImplementation(requestMatches(urlOrPredicate));
|
|
if (bodyOrFunction) {
|
|
fetch.mockResponse(bodyOrFunction, init);
|
|
}
|
|
return fetch;
|
|
};
|
|
|
|
fetch.dontMockIf = (urlOrPredicate, bodyOrFunction, init) => {
|
|
isMocking.mockImplementation(requestNotMatches(urlOrPredicate));
|
|
if (bodyOrFunction) {
|
|
fetch.mockResponse(bodyOrFunction, init);
|
|
}
|
|
return fetch;
|
|
};
|
|
|
|
fetch.mockOnceIf = fetch.doMockOnceIf = (urlOrPredicate, bodyOrFunction, init) => {
|
|
isMocking.mockImplementationOnce(requestMatches(urlOrPredicate));
|
|
if (bodyOrFunction) {
|
|
mockResponseOnce(bodyOrFunction, init);
|
|
}
|
|
return fetch;
|
|
};
|
|
|
|
fetch.dontMockOnceIf = (urlOrPredicate, bodyOrFunction, init) => {
|
|
isMocking.mockImplementationOnce(requestNotMatches(urlOrPredicate));
|
|
if (bodyOrFunction) {
|
|
mockResponseOnce(bodyOrFunction, init);
|
|
}
|
|
return fetch;
|
|
};
|
|
|
|
fetch.dontMock = () => {
|
|
isMocking.mockImplementation(staticMatches(false));
|
|
return fetch;
|
|
};
|
|
|
|
fetch.dontMockOnce = () => {
|
|
isMocking.mockImplementationOnce(staticMatches(false));
|
|
return fetch;
|
|
};
|
|
|
|
fetch.doMock = (bodyOrFunction, init) => {
|
|
isMocking.mockImplementation(staticMatches(true));
|
|
if (bodyOrFunction) {
|
|
fetch.mockResponse(bodyOrFunction, init);
|
|
}
|
|
return fetch;
|
|
};
|
|
|
|
fetch.mockOnce = fetch.doMockOnce = (bodyOrFunction, init) => {
|
|
isMocking.mockImplementationOnce(staticMatches(true));
|
|
if (bodyOrFunction) {
|
|
mockResponseOnce(bodyOrFunction, init);
|
|
}
|
|
return fetch;
|
|
};
|
|
|
|
fetch.requests = () => {
|
|
const requests = [];
|
|
fetch.mock.calls.forEach((call) => {
|
|
try {
|
|
let req = normalizeRequest(call[0], call[1]);
|
|
requests.push(req);
|
|
} catch(e) {
|
|
// ignore
|
|
}
|
|
});
|
|
return requests;
|
|
};
|
|
|
|
fetch.resetMocks = () => {
|
|
fetch.mockReset();
|
|
isMocking.mockReset();
|
|
|
|
// reset to default implementation with each reset
|
|
fetch.mockImplementation(normalizeResponse(''));
|
|
fetch.doMock();
|
|
fetch.isMocking = (req, reqInit) => isMocking(req, reqInit)[0];
|
|
};
|
|
|
|
fetch.enableMocks = () => {
|
|
globalThis.fetchMock = globalThis.fetch = fetch;
|
|
};
|
|
|
|
fetch.disableMocks = () => {
|
|
globalThis.fetch = crossFetch;
|
|
};
|
|
|
|
return fetch;
|
|
}
|