From 76cb9c2a39d477a64824a985ade40507e3bbade1 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Fri, 13 Feb 2026 21:34:48 -0800 Subject: feat(vanilla): add testing infrastructure and tests (NK-wjnczv) --- vanilla/node_modules/jsdom/lib/api.js | 373 ++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 vanilla/node_modules/jsdom/lib/api.js (limited to 'vanilla/node_modules/jsdom/lib/api.js') diff --git a/vanilla/node_modules/jsdom/lib/api.js b/vanilla/node_modules/jsdom/lib/api.js new file mode 100644 index 0000000..6040d4f --- /dev/null +++ b/vanilla/node_modules/jsdom/lib/api.js @@ -0,0 +1,373 @@ +"use strict"; +const path = require("path"); +const { pathToFileURL } = require("url"); +const fs = require("fs").promises; +const vm = require("vm"); +const toughCookie = require("tough-cookie"); +const sniffHTMLEncoding = require("html-encoding-sniffer"); +const whatwgURL = require("whatwg-url"); +const { legacyHookDecode } = require("@exodus/bytes/encoding.js"); +const { URL } = require("whatwg-url"); +const { MIMEType } = require("whatwg-mimetype"); +const { getGlobalDispatcher } = require("undici"); +const idlUtils = require("./jsdom/living/generated/utils.js"); +const VirtualConsole = require("./jsdom/virtual-console.js"); +const { createWindow } = require("./jsdom/browser/Window.js"); +const { parseIntoDocument } = require("./jsdom/browser/parser"); +const { fragmentSerialization } = require("./jsdom/living/domparsing/serialization.js"); +const createDecompressInterceptor = require("./jsdom/browser/resources/decompress-interceptor.js"); +const { + JSDOMDispatcher, DEFAULT_USER_AGENT, fetchCollected +} = require("./jsdom/browser/resources/jsdom-dispatcher.js"); +const requestInterceptor = require("./jsdom/browser/resources/request-interceptor.js"); + +class CookieJar extends toughCookie.CookieJar { + constructor(store, options) { + // jsdom cookie jars must be loose by default + super(store, { looseMode: true, ...options }); + } +} + +const window = Symbol("window"); +let sharedFragmentDocument = null; + +class JSDOM { + constructor(input = "", options = {}) { + const mimeType = new MIMEType(options.contentType === undefined ? "text/html" : options.contentType); + const { html, encoding } = normalizeHTML(input, mimeType); + + options = transformOptions(options, encoding, mimeType); + + this[window] = createWindow(options.windowOptions); + + const documentImpl = idlUtils.implForWrapper(this[window]._document); + + options.beforeParse(this[window]._globalProxy); + + parseIntoDocument(html, documentImpl); + + documentImpl.close(); + } + + get window() { + // It's important to grab the global proxy, instead of just the result of `createWindow(...)`, since otherwise + // things like `window.eval` don't exist. + return this[window]._globalProxy; + } + + get virtualConsole() { + return this[window]._virtualConsole; + } + + get cookieJar() { + // TODO NEWAPI move _cookieJar to window probably + return idlUtils.implForWrapper(this[window]._document)._cookieJar; + } + + serialize() { + return fragmentSerialization(idlUtils.implForWrapper(this[window]._document), { requireWellFormed: false }); + } + + nodeLocation(node) { + if (!idlUtils.implForWrapper(this[window]._document)._parseOptions.sourceCodeLocationInfo) { + throw new Error("Location information was not saved for this jsdom. Use includeNodeLocations during creation."); + } + + return idlUtils.implForWrapper(node).sourceCodeLocation; + } + + getInternalVMContext() { + if (!vm.isContext(this[window])) { + throw new TypeError("This jsdom was not configured to allow script running. " + + "Use the runScripts option during creation."); + } + + return this[window]; + } + + reconfigure(settings) { + if ("windowTop" in settings) { + this[window]._top = settings.windowTop; + } + + if ("url" in settings) { + const document = idlUtils.implForWrapper(this[window]._document); + + const url = whatwgURL.parseURL(settings.url); + if (url === null) { + throw new TypeError(`Could not parse "${settings.url}" as a URL`); + } + + document._URL = url; + document._origin = whatwgURL.serializeURLOrigin(document._URL); + this[window]._sessionHistory.currentEntry.url = url; + document._clearBaseURLCache(); + } + } + + static fragment(string = "") { + if (!sharedFragmentDocument) { + sharedFragmentDocument = (new JSDOM()).window.document; + } + + const template = sharedFragmentDocument.createElement("template"); + template.innerHTML = string; + return template.content; + } + + static async fromURL(url, options = {}) { + options = normalizeFromURLOptions(options); + + // Build the dispatcher for the initial request + // For the initial fetch, we default to "usable" instead of no resource loading, since fromURL() implicitly requests + // fetching the initial resource. This does not impact further resource fetching, which uses options.resources. + const resourcesForInitialFetch = options.resources !== undefined ? options.resources : "usable"; + const { effectiveDispatcher } = extractResourcesOptions(resourcesForInitialFetch, options.cookieJar); + + const headers = { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" }; + if (options.referrer) { + headers.Referer = options.referrer; + } + + const response = await fetchCollected(effectiveDispatcher, { + url, + headers + }); + + if (!response.ok) { + throw new Error(`Resource was not loaded. Status: ${response.status}`); + } + + options = Object.assign(options, { + url: response.url, + contentType: response.headers["content-type"] || undefined, + referrer: options.referrer, + resources: options.resources + }); + + return new JSDOM(response.body, options); + } + + static async fromFile(filename, options = {}) { + options = normalizeFromFileOptions(filename, options); + const nodeBuffer = await fs.readFile(filename); + + return new JSDOM(nodeBuffer, options); + } +} + +function normalizeFromURLOptions(options) { + // Checks on options that are invalid for `fromURL` + if (options.url !== undefined) { + throw new TypeError("Cannot supply a url option when using fromURL"); + } + if (options.contentType !== undefined) { + throw new TypeError("Cannot supply a contentType option when using fromURL"); + } + + // Normalization of options which must be done before the rest of the fromURL code can use them, because they are + // given to request() + const normalized = { ...options }; + + if (options.referrer !== undefined) { + normalized.referrer = (new URL(options.referrer)).href; + } + + if (options.cookieJar === undefined) { + normalized.cookieJar = new CookieJar(); + } + + return normalized; + + // All other options don't need to be processed yet, and can be taken care of in the normal course of things when + // `fromURL` calls `new JSDOM(html, options)`. +} + +function extractResourcesOptions(resources, cookieJar) { + // loadSubresources controls whether PerDocumentResourceLoader fetches scripts, stylesheets, etc. + // XHR always works regardless of this flag. + let userAgent, baseDispatcher, userInterceptors, loadSubresources; + + if (resources === undefined) { + // resources: undefined means no automatic subresource fetching, but XHR still works + userAgent = DEFAULT_USER_AGENT; + baseDispatcher = getGlobalDispatcher(); + userInterceptors = []; + loadSubresources = false; + } else if (resources === "usable") { + // resources: "usable" means use all defaults + userAgent = DEFAULT_USER_AGENT; + baseDispatcher = getGlobalDispatcher(); + userInterceptors = []; + loadSubresources = true; + } else if (typeof resources === "object" && resources !== null) { + // resources: { userAgent?, dispatcher?, interceptors? } + userAgent = resources.userAgent !== undefined ? resources.userAgent : DEFAULT_USER_AGENT; + baseDispatcher = resources.dispatcher !== undefined ? resources.dispatcher : getGlobalDispatcher(); + userInterceptors = resources.interceptors !== undefined ? resources.interceptors : []; + loadSubresources = true; + } else { + throw new TypeError(`resources must be undefined, "usable", or an object`); + } + + // User interceptors come first (outermost), then decompress interceptor + const allUserInterceptors = [ + ...userInterceptors, + createDecompressInterceptor() + ]; + + return { + userAgent, + effectiveDispatcher: new JSDOMDispatcher({ + baseDispatcher, + cookieJar, + userAgent, + userInterceptors: allUserInterceptors + }), + loadSubresources + }; +} + +function normalizeFromFileOptions(filename, options) { + const normalized = { ...options }; + + if (normalized.contentType === undefined) { + const extname = path.extname(filename); + if (extname === ".xhtml" || extname === ".xht" || extname === ".xml") { + normalized.contentType = "application/xhtml+xml"; + } + } + + if (normalized.url === undefined) { + normalized.url = pathToFileURL(path.resolve(filename)).href; + } + + return normalized; +} + +function transformOptions(options, encoding, mimeType) { + const transformed = { + windowOptions: { + // Defaults + url: "about:blank", + referrer: "", + contentType: "text/html", + parsingMode: "html", + parseOptions: { + sourceCodeLocationInfo: false, + scriptingEnabled: false + }, + runScripts: undefined, + encoding, + pretendToBeVisual: false, + storageQuota: 5000000, + + // Defaults filled in later + dispatcher: undefined, + loadSubresources: undefined, + userAgent: undefined, + virtualConsole: undefined, + cookieJar: undefined + }, + + // Defaults + beforeParse() { } + }; + + // options.contentType was parsed into mimeType by the caller. + if (!mimeType.isHTML() && !mimeType.isXML()) { + throw new RangeError(`The given content type of "${options.contentType}" was not a HTML or XML content type`); + } + + transformed.windowOptions.contentType = mimeType.essence; + transformed.windowOptions.parsingMode = mimeType.isHTML() ? "html" : "xml"; + + if (options.url !== undefined) { + transformed.windowOptions.url = (new URL(options.url)).href; + } + + if (options.referrer !== undefined) { + transformed.windowOptions.referrer = (new URL(options.referrer)).href; + } + + if (options.includeNodeLocations) { + if (transformed.windowOptions.parsingMode === "xml") { + throw new TypeError("Cannot set includeNodeLocations to true with an XML content type"); + } + + transformed.windowOptions.parseOptions = { sourceCodeLocationInfo: true }; + } + + transformed.windowOptions.cookieJar = options.cookieJar === undefined ? + new CookieJar() : + options.cookieJar; + + transformed.windowOptions.virtualConsole = options.virtualConsole === undefined ? + (new VirtualConsole()).forwardTo(console) : + options.virtualConsole; + + if (!(transformed.windowOptions.virtualConsole instanceof VirtualConsole)) { + throw new TypeError("virtualConsole must be an instance of VirtualConsole"); + } + + const { userAgent, effectiveDispatcher, loadSubresources } = + extractResourcesOptions(options.resources, transformed.windowOptions.cookieJar); + transformed.windowOptions.userAgent = userAgent; + transformed.windowOptions.dispatcher = effectiveDispatcher; + transformed.windowOptions.loadSubresources = loadSubresources; + + if (options.runScripts !== undefined) { + transformed.windowOptions.runScripts = String(options.runScripts); + if (transformed.windowOptions.runScripts === "dangerously") { + transformed.windowOptions.parseOptions.scriptingEnabled = true; + } else if (transformed.windowOptions.runScripts !== "outside-only") { + throw new RangeError(`runScripts must be undefined, "dangerously", or "outside-only"`); + } + } + + if (options.beforeParse !== undefined) { + transformed.beforeParse = options.beforeParse; + } + + if (options.pretendToBeVisual !== undefined) { + transformed.windowOptions.pretendToBeVisual = Boolean(options.pretendToBeVisual); + } + + if (options.storageQuota !== undefined) { + transformed.windowOptions.storageQuota = Number(options.storageQuota); + } + + return transformed; +} + +function normalizeHTML(html, mimeType) { + let encoding = "UTF-8"; + + if (html instanceof Uint8Array) { + // leave as-is + } else if (ArrayBuffer.isView(html)) { + html = new Uint8Array(html.buffer, html.byteOffset, html.byteLength); + } else if (html instanceof ArrayBuffer) { + html = new Uint8Array(html); + } + + if (html instanceof Uint8Array) { + encoding = sniffHTMLEncoding(html, { + xml: mimeType.isXML(), + transportLayerEncodingLabel: mimeType.parameters.get("charset") + }); + html = legacyHookDecode(html, encoding); + } else { + html = String(html); + } + + return { html, encoding }; +} + +exports.JSDOM = JSDOM; + +exports.VirtualConsole = VirtualConsole; +exports.CookieJar = CookieJar; +exports.requestInterceptor = requestInterceptor; + +exports.toughCookie = toughCookie; -- cgit v1.2.3