 aacb45156b
			
		
	
	aacb45156b
	
	
	
		
			
			- Install Jest for unit testing with React Testing Library - Install Playwright for end-to-end testing - Configure Jest with proper TypeScript support and module mapping - Create test setup files and utilities for both unit and e2e tests Components: * Jest configuration with coverage thresholds * Playwright configuration with browser automation * Unit tests for LoginForm, AuthContext, and useSocketIO hook * E2E tests for authentication, dashboard, and agents workflows * GitHub Actions workflow for automated testing * Mock data and API utilities for consistent testing * Test documentation with best practices Testing features: - Unit tests with 70% coverage threshold - E2E tests with API mocking and user journey testing - CI/CD integration for automated test runs - Cross-browser testing support with Playwright - Authentication system testing end-to-end 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			335 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			335 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "use strict";
 | |
| const path = require("path");
 | |
| 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 whatwgEncoding = require("whatwg-encoding");
 | |
| const { URL } = require("whatwg-url");
 | |
| const MIMEType = require("whatwg-mimetype");
 | |
| 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 ResourceLoader = require("./jsdom/browser/resources/resource-loader.js");
 | |
| const NoOpResourceLoader = require("./jsdom/browser/resources/no-op-resource-loader.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;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   static fragment(string = "") {
 | |
|     if (!sharedFragmentDocument) {
 | |
|       sharedFragmentDocument = (new JSDOM()).window.document;
 | |
|     }
 | |
| 
 | |
|     const template = sharedFragmentDocument.createElement("template");
 | |
|     template.innerHTML = string;
 | |
|     return template.content;
 | |
|   }
 | |
| 
 | |
|   static fromURL(url, options = {}) {
 | |
|     return Promise.resolve().then(() => {
 | |
|       // Remove the hash while sending this through the research loader fetch().
 | |
|       // It gets added back a few lines down when constructing the JSDOM object.
 | |
|       const parsedURL = new URL(url);
 | |
|       const originalHash = parsedURL.hash;
 | |
|       parsedURL.hash = "";
 | |
|       url = parsedURL.href;
 | |
| 
 | |
|       options = normalizeFromURLOptions(options);
 | |
| 
 | |
|       const resourceLoader = resourcesToResourceLoader(options.resources);
 | |
|       const resourceLoaderForInitialRequest = resourceLoader.constructor === NoOpResourceLoader ?
 | |
|         new ResourceLoader() :
 | |
|         resourceLoader;
 | |
| 
 | |
|       const req = resourceLoaderForInitialRequest.fetch(url, {
 | |
|         accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
 | |
|         cookieJar: options.cookieJar,
 | |
|         referrer: options.referrer
 | |
|       });
 | |
| 
 | |
|       return req.then(body => {
 | |
|         const res = req.response;
 | |
| 
 | |
|         options = Object.assign(options, {
 | |
|           url: req.href + originalHash,
 | |
|           contentType: res.headers["content-type"],
 | |
|           referrer: req.getHeader("referer") ?? undefined
 | |
|         });
 | |
| 
 | |
|         return new JSDOM(body, options);
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   static async fromFile(filename, options = {}) {
 | |
|     options = normalizeFromFileOptions(filename, options);
 | |
|     const buffer = await fs.readFile(filename);
 | |
| 
 | |
|     return new JSDOM(buffer, 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 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 = new URL("file:" + path.resolve(filename));
 | |
|   }
 | |
| 
 | |
|   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
 | |
|       resourceLoader: 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()).sendTo(console) :
 | |
|                                             options.virtualConsole;
 | |
| 
 | |
|   if (!(transformed.windowOptions.virtualConsole instanceof VirtualConsole)) {
 | |
|     throw new TypeError("virtualConsole must be an instance of VirtualConsole");
 | |
|   }
 | |
| 
 | |
|   transformed.windowOptions.resourceLoader = resourcesToResourceLoader(options.resources);
 | |
| 
 | |
|   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 (ArrayBuffer.isView(html)) {
 | |
|     html = Buffer.from(html.buffer, html.byteOffset, html.byteLength);
 | |
|   } else if (html instanceof ArrayBuffer) {
 | |
|     html = Buffer.from(html);
 | |
|   }
 | |
| 
 | |
|   if (Buffer.isBuffer(html)) {
 | |
|     encoding = sniffHTMLEncoding(html, {
 | |
|       defaultEncoding: mimeType.isXML() ? "UTF-8" : "windows-1252",
 | |
|       transportLayerEncodingLabel: mimeType.parameters.get("charset")
 | |
|     });
 | |
|     html = whatwgEncoding.decode(html, encoding);
 | |
|   } else {
 | |
|     html = String(html);
 | |
|   }
 | |
| 
 | |
|   return { html, encoding };
 | |
| }
 | |
| 
 | |
| function resourcesToResourceLoader(resources) {
 | |
|   switch (resources) {
 | |
|     case undefined: {
 | |
|       return new NoOpResourceLoader();
 | |
|     }
 | |
|     case "usable": {
 | |
|       return new ResourceLoader();
 | |
|     }
 | |
|     default: {
 | |
|       if (!(resources instanceof ResourceLoader)) {
 | |
|         throw new TypeError("resources must be an instance of ResourceLoader");
 | |
|       }
 | |
|       return resources;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| exports.JSDOM = JSDOM;
 | |
| 
 | |
| exports.VirtualConsole = VirtualConsole;
 | |
| exports.CookieJar = CookieJar;
 | |
| exports.ResourceLoader = ResourceLoader;
 | |
| 
 | |
| exports.toughCookie = toughCookie;
 |