UNPKG

mock-violentmonkey

Version:

Mock violentmonkey's globals for testing userscripts

197 lines 6.69 kB
import { Buffer } from 'node:buffer'; import crypto from 'node:crypto'; import { JSDOM } from 'jsdom'; import { getBaseUrl } from '../base-url.js'; import { getWindow } from '../dom.js'; import { XMLHttpRequest, } from '../xmlhttprequest/index.js'; const formDataToBuffer = async (data) => { const boundary = '-'.repeat(20) + crypto.randomBytes(20).toString('hex'); const body = []; for (const [key, value] of data) { body.push(`--${boundary}`, `Content-Disposition: form-data; name=${JSON.stringify(key)}`); if (typeof value === 'string') { // Two CRLF body.push('', value); } else { await new Promise((resolve, reject) => { const fr = new (getWindow().FileReader)(); fr.addEventListener('load', () => { const result = fr.result; // eslint-disable-next-line unicorn/prefer-at body[body.length - 1] += `; filename=${JSON.stringify(value.name)}`; // Two CRLF before result body.push(`Content-Type: ${value.type || 'application/octet-stream'}`, '', result); resolve(); }); fr.addEventListener('error', () => { reject(fr.error); }); // For some reason I can't figure out, Blob#text() just times out // eslint-disable-next-line unicorn/prefer-blob-reading-methods fr.readAsText(value); }); } } // Trailing CRLF body.push(`--${boundary}--`, ''); const contentType = `multipart/form-data; boundary=${boundary}`; return { content: Buffer.from(body.join('\r\n')), contentType, }; }; const dataToBuffer = async (data) => { if (data === undefined) { return { content: undefined, contentType: undefined, }; } if (typeof data === 'string') { return { content: Buffer.from(data), contentType: 'text/plain;charset=UTF-8', }; } if (data instanceof getWindow().FormData || (typeof FormData !== 'undefined' && data instanceof FormData)) { return formDataToBuffer(data); } if (data instanceof Blob) { return { content: Buffer.from(await data.arrayBuffer()), contentType: data.type || undefined, }; } return dataToBuffer(String(data)); }; const responseToResponseType = (xhr, responseType) => { responseType = String(responseType).toLowerCase(); const buffer = xhr.responseBuffer; switch (String(responseType).toLowerCase()) { case 'arraybuffer': { const arrayBuffer = new ArrayBuffer(buffer.byteLength); const view = new Uint8Array(arrayBuffer); for (const [index, value] of buffer.entries()) { view[index] = value; } return arrayBuffer; } case 'json': { try { return JSON.parse(buffer.toString()); } catch (error) { return error; } } case 'blob': { return new Blob([buffer], { type: xhr.getResponseHeader('content-type'), }); } case 'document': { return new JSDOM(buffer.toString(), { url: xhr.responseURL, }).window.document; } default: { return buffer.toString(); } } }; const makeEventResponse = (xhr, details) => { // Caching this one is necessary because otherwise // `responseObject.response === responseObject.response` returns `false` // if they are some form of object let response; // Caching this one is optional, since comparing two equal strings returns `true` let responseText; return Object.defineProperties({ status: xhr.status, statusText: xhr.statusText, readyState: xhr.readyState, responseHeaders: xhr.getAllResponseHeaders(), finalUrl: xhr.responseURL, context: details.context, }, { response: { get() { response ??= responseToResponseType(xhr, details.responseType); return response; }, }, responseText: { get() { responseText ??= xhr.responseBuffer.toString(); return responseText; }, }, }); }; const xmlhttpRequest = (details) => { let aborted = false; // eslint-disable-next-line promise/prefer-await-to-then void dataToBuffer(details.data).then(({ content, contentType }) => { const xhr = new XMLHttpRequest({ base: getBaseUrl(), }); aborter.abort = () => { xhr.abort(); }; if (typeof details.timeout === 'number') { xhr.timeout = details.timeout; } const events = [ 'abort', 'error', 'load', 'loadend', 'loadstart', 'progress', 'readystatechange', 'timeout', ]; for (const event of events) { xhr.addEventListener(event, () => { const callback = details[`on${event}`]; if (typeof callback === 'function') { // eslint-disable-next-line promise/no-callback-in-promise void callback(makeEventResponse(xhr, details)); } }); } xhr.open(details.method ?? 'get', details.url, details.user, details.password); if (details.headers) { for (const [key, value] of Object.entries(details.headers)) { xhr.setRequestHeader(key, value); } } if (contentType !== undefined && !xhr.getRequestHeader('content-type')) { xhr.setRequestHeader('content-type', contentType); } xhr.send(content); // Doing it like this makes sure that // it behaves just like asynchronous abort if (aborted) { xhr.abort(); } }); // Once xhr exists switch this out // This is necessary because turning the data to a buffer // is asynchronous but GM_xmlhttpRequest not const aborter = { abort() { aborted = true; }, }; return aborter; }; export { xmlhttpRequest as GM_xmlhttpRequest, }; Object.defineProperties(globalThis, { GM_xmlhttpRequest: { value: xmlhttpRequest, }, }); //# sourceMappingURL=xmlhttprequest.js.map