mirror of
https://github.com/bigskysoftware/htmx.git
synced 2026-01-13 04:01:57 +00:00
230 lines
7.0 KiB
JavaScript
230 lines
7.0 KiB
JavaScript
/**
|
|
* Mock implementation of the Fetch API for testing
|
|
*/
|
|
|
|
class MockResponse {
|
|
constructor(body, init = {}) {
|
|
this.body = body;
|
|
this.status = init.status || 200;
|
|
this.ok = this.status >= 200 && this.status < 300;
|
|
this.statusText = init.statusText || '';
|
|
this.headers = new Map(Object.entries(init.headers || {}));
|
|
this.url = init.url || '';
|
|
this.type = init.type || 'basic';
|
|
}
|
|
|
|
async json() {
|
|
if (typeof this.body === 'string') {
|
|
return JSON.parse(this.body);
|
|
}
|
|
return this.body;
|
|
}
|
|
|
|
async text() {
|
|
if (typeof this.body === 'string') {
|
|
return this.body;
|
|
}
|
|
return JSON.stringify(this.body);
|
|
}
|
|
|
|
async blob() {
|
|
return new Blob([await this.text()]);
|
|
}
|
|
|
|
async arrayBuffer() {
|
|
const text = await this.text();
|
|
const encoder = new TextEncoder();
|
|
return encoder.encode(text).buffer;
|
|
}
|
|
|
|
clone() {
|
|
return new MockResponse(this.body, {
|
|
status: this.status,
|
|
statusText: this.statusText,
|
|
headers: Object.fromEntries(this.headers),
|
|
url: this.url,
|
|
type: this.type
|
|
});
|
|
}
|
|
}
|
|
|
|
class FetchMock {
|
|
constructor() {
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
// Abort all pending requests before clearing
|
|
if (this.pendingRequests) {
|
|
this.pendingRequests.forEach(({ controller }) => {
|
|
if (controller && !controller.signal.aborted) {
|
|
controller.abort();
|
|
}
|
|
});
|
|
}
|
|
this.calls = [];
|
|
this.responses = [];
|
|
this.pendingRequests = [];
|
|
}
|
|
|
|
// Record a fetch call
|
|
recordCall(url, options) {
|
|
this.calls.push({ url, request: options });
|
|
}
|
|
|
|
// Get all recorded calls
|
|
getCalls() {
|
|
return this.calls;
|
|
}
|
|
|
|
// Get the last call
|
|
getLastCall() {
|
|
return this.calls[this.calls.length - 1];
|
|
}
|
|
|
|
// Mock a response for a specific URL pattern
|
|
mockResponse(method, urlPattern, response, options = {}) {
|
|
let upperCasedMethod = method.toUpperCase();
|
|
if (['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(upperCasedMethod) < 0) {
|
|
throw Error("Invalid HTTP method: " + method)
|
|
}
|
|
if (typeof response === 'string') {
|
|
let str = response;
|
|
response = new MockResponse(str, options);
|
|
}
|
|
this.responses.push({
|
|
method: upperCasedMethod,
|
|
urlPattern: typeof urlPattern === 'string' ? new RegExp(urlPattern) : urlPattern,
|
|
response,
|
|
request: options,
|
|
once: options.once || false,
|
|
used: false
|
|
});
|
|
}
|
|
|
|
// Mock a successful JSON response
|
|
mockJsonResponse(method, urlPattern, data, status = 200) {
|
|
this.mockResponse(method, urlPattern, new MockResponse(data, {
|
|
status,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}));
|
|
}
|
|
|
|
// Mock an error response
|
|
mockErrorResponse(method, urlPattern, status = 500, message = 'Server Error') {
|
|
this.mockResponse(method, urlPattern, new MockResponse({ error: message }, { status }));
|
|
}
|
|
|
|
// Mock a network error
|
|
mockNetworkError(method, urlPattern, error = new Error('Network Error')) {
|
|
this.mockResponse(method, urlPattern, Promise.reject(error));
|
|
}
|
|
|
|
// Mock a network failure (simpler alias)
|
|
mockFailure(method, urlPattern, message = 'Network failure') {
|
|
this.mockNetworkError(method, urlPattern, new Error(message));
|
|
}
|
|
|
|
// Find matching response
|
|
findResponse(method, url) {
|
|
for (let i = this.responses.length - 1; i >= 0; i--) {
|
|
const mock = this.responses[i];
|
|
if (mock.method === method && mock.urlPattern.test(url)) {
|
|
if (mock.once) {
|
|
if (!mock.used) {
|
|
mock.used = true;
|
|
return typeof mock.response === 'function' ? mock.response() : mock.response;
|
|
}
|
|
} else {
|
|
return typeof mock.response === 'function' ? mock.response() : mock.response;
|
|
}
|
|
}
|
|
}
|
|
console.error("no response configured for ", url, " available responses: ", this.responses);
|
|
return "NO RESPONSE CONFIGURED FOR " + url;
|
|
}
|
|
|
|
// Wait for all pending requests to complete
|
|
async waitForRequests() {
|
|
if (this.pendingRequests.length === 0) {
|
|
return Promise.resolve();
|
|
}
|
|
await Promise.all(this.pendingRequests.map(req => req.promise));
|
|
}
|
|
|
|
// The actual fetch mock function
|
|
fetch(url, options = {}) {
|
|
this.recordCall(url, options);
|
|
options.method = options.method.toUpperCase()
|
|
const response = this.findResponse(options.method, url);
|
|
|
|
// Create an AbortController for this request
|
|
const controller = new AbortController();
|
|
|
|
// Create a tracking object for this request
|
|
const pendingRequest = { controller, promise: null };
|
|
|
|
// Create a promise to track this request
|
|
const requestPromise = new Promise((resolve, reject) => {
|
|
// Check if already aborted
|
|
if (controller.signal.aborted) {
|
|
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
return;
|
|
}
|
|
|
|
// Listen for abort
|
|
controller.signal.addEventListener('abort', () => {
|
|
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
});
|
|
|
|
// Process the response
|
|
Promise.resolve(response instanceof Promise ? response : response)
|
|
.then(result => {
|
|
if (!controller.signal.aborted) {
|
|
resolve(result);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (!controller.signal.aborted) {
|
|
reject(error);
|
|
}
|
|
});
|
|
})
|
|
.finally(() => {
|
|
// Remove from pending requests when done
|
|
const index = this.pendingRequests.indexOf(pendingRequest);
|
|
if (index > -1) {
|
|
this.pendingRequests.splice(index, 1);
|
|
}
|
|
});
|
|
|
|
// Store the promise in the tracking object
|
|
pendingRequest.promise = requestPromise;
|
|
|
|
// Track this pending request
|
|
this.pendingRequests.push(pendingRequest);
|
|
|
|
return requestPromise;
|
|
}
|
|
}
|
|
|
|
// Create a global instance
|
|
const fetchMock = new FetchMock();
|
|
|
|
// Install the mock globally
|
|
function installFetchMock() {
|
|
globalThis.fetch = fetchMock.fetch.bind(fetchMock);
|
|
}
|
|
|
|
// Uninstall the mock and restore original fetch
|
|
let originalFetch;
|
|
function uninstallFetchMock() {
|
|
if (originalFetch) {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
}
|
|
|
|
// Save original fetch before installing
|
|
if (typeof globalThis.fetch !== 'undefined') {
|
|
originalFetch = globalThis.fetch;
|
|
} |