mock-xmlhttprequest
Version:
XMLHttpRequest mock for testing
150 lines (146 loc) • 6.01 kB
JavaScript
/**
* mock-xmlhttprequest v8.4.1
* (c) 2025 Bertrand Guay-Paquet
* @license MIT
*/
'use strict';
/**
* Implementation of XMLHttpRequestEventTarget. A target for dispatching events.
*
* See https://xhr.spec.whatwg.org/#xmlhttprequesteventtarget
*/
class XhrEventTarget {
constructor(eventContext) {
this._eventContext = eventContext ?? this;
this._listeners = new Map();
}
get onabort() { return this._getEventHandlerProperty('abort'); }
set onabort(value) { this._setEventHandlerProperty('abort', value); }
get onerror() { return this._getEventHandlerProperty('error'); }
set onerror(value) { this._setEventHandlerProperty('error', value); }
get onload() { return this._getEventHandlerProperty('load'); }
set onload(value) { this._setEventHandlerProperty('load', value); }
get onloadend() { return this._getEventHandlerProperty('loadend'); }
set onloadend(value) { this._setEventHandlerProperty('loadend', value); }
get onloadstart() { return this._getEventHandlerProperty('loadstart'); }
set onloadstart(value) { this._setEventHandlerProperty('loadstart', value); }
get onprogress() { return this._getEventHandlerProperty('progress'); }
set onprogress(value) { this._setEventHandlerProperty('progress', value); }
get ontimeout() { return this._getEventHandlerProperty('timeout'); }
set ontimeout(value) { this._setEventHandlerProperty('timeout', value); }
/**
* @returns Whether any event listener is registered
*/
hasListeners() {
return [...this._listeners.values()].some((listeners) => listeners.length > 0);
}
addEventListener(type, listener, options) {
if (listener) {
const listenerEntry = makeListenerEntry(listener, false, options);
const listeners = this._listeners.get(type) ?? [];
// If eventTarget’s event listener list does not contain an event listener whose type is
// listener’s type, callback is listener’s callback, and capture is listener’s capture, then
// append listener to eventTarget’s event listener list.
// See https://dom.spec.whatwg.org/#add-an-event-listener
if (listeners.every(({ isEventHandlerProperty, listener, useCapture }) => {
return isEventHandlerProperty ||
listenerEntry.listener !== listener ||
listenerEntry.useCapture !== useCapture;
})) {
listeners.push(listenerEntry);
this._listeners.set(type, listeners);
}
}
}
removeEventListener(type, listener, options) {
if (listener) {
const listeners = this._listeners.get(type);
if (listeners) {
const listenerEntry = makeListenerEntry(listener, false, options);
const index = listeners.findIndex(({ isEventHandlerProperty, listener, useCapture }) => {
return !isEventHandlerProperty &&
listenerEntry.listener === listener &&
listenerEntry.useCapture === useCapture;
});
if (index >= 0) {
listeners[index].removed = true;
listeners.splice(index, 1);
}
}
}
}
/**
* Calls all the listeners for the event.
*
* @param event Event
* @returns Always true since none of the xhr event are cancelable
*/
dispatchEvent(event) {
// Only the event listeners registered at this point should be called. Storing them here avoids
// problems with callbacks that add or remove listeners.
const listeners = this._listeners.get(event.type);
if (listeners) {
[...listeners].forEach((listenerEntry) => {
if (!listenerEntry.removed) {
if (listenerEntry.once) {
const index = listeners.indexOf(listenerEntry);
if (index >= 0) {
listeners.splice(index, 1);
}
}
if (typeof listenerEntry.listener === 'function') {
listenerEntry.listener.call(this._eventContext, event);
}
else {
listenerEntry.listener.handleEvent(event);
}
}
});
}
return true;
}
_getEventHandlerProperty(event) {
const listeners = this._listeners.get(event);
if (listeners) {
const entry = listeners.find((entry) => entry.isEventHandlerProperty);
if (entry) {
return entry.listener;
}
}
return null;
}
_setEventHandlerProperty(event, value) {
const listeners = this._listeners.get(event);
if (listeners) {
const index = listeners.findIndex((entry) => entry.isEventHandlerProperty);
if (index >= 0) {
if (listeners[index].listener === value) {
// no change
return;
}
listeners[index].removed = true;
listeners.splice(index, 1);
}
}
if (value) {
const listenerEntry = makeListenerEntry(value, true);
if (listeners) {
listeners.push(listenerEntry);
}
else {
this._listeners.set(event, [listenerEntry]);
}
}
}
}
function makeListenerEntry(listener, isEventHandlerProperty, options) {
const optionsIsBoolean = typeof options === 'boolean';
return {
listener,
isEventHandlerProperty,
useCapture: optionsIsBoolean ? options : !!options?.capture,
once: optionsIsBoolean ? false : !!options?.once,
removed: false,
};
}
module.exports = XhrEventTarget;