/** * Window Mock * * The global object for browser environments. * This ties everything together. */ import windowStubs from '../stubs/window_stubs.json' with { type: 'json' }; import chromeProps from '../stubs/chrome_props.json' with { type: 'json' }; import { createScreen } from './screen.js'; import { createNavigator } from './navigator.js'; import { createDocument } from './document.js'; import { createPerformance } from './performance.js'; import { createCrypto } from './crypto.js'; import { createStorage } from './storage.js'; export function createWindow(fingerprint = {}) { const stubs = { ...windowStubs, ...fingerprint.window }; const screen = createScreen(fingerprint.screen); const navigator = createNavigator(fingerprint.navigator); const document = createDocument(fingerprint); const performance = createPerformance(fingerprint); const crypto = createCrypto(); const localStorage = createStorage(); const sessionStorage = createStorage(); const eventListeners = new Map(); let origin = fingerprint.origin || 'https://example.com'; const location = createLocation(fingerprint.url || 'https://example.com/'); const history = { length: 1, scrollRestoration: 'auto', state: null, back() {}, forward() {}, go() {}, pushState() {}, replaceState() {}, }; const win = { // Window identity window: null, // Self-reference, set below self: null, top: null, parent: null, globalThis: null, frames: [], length: 0, frameElement: null, opener: null, closed: false, name: '', // Core objects document, navigator, screen, location, history, performance, crypto, localStorage, sessionStorage, // Visual viewport visualViewport: { width: stubs.innerWidth, height: stubs.innerHeight, offsetLeft: 0, offsetTop: 0, pageLeft: 0, pageTop: 0, scale: 1, addEventListener() {}, removeEventListener() {}, }, // Dimensions innerWidth: stubs.innerWidth, innerHeight: stubs.innerHeight, outerWidth: stubs.outerWidth, outerHeight: stubs.outerHeight, devicePixelRatio: stubs.devicePixelRatio, // Scroll pageXOffset: stubs.pageXOffset, pageYOffset: stubs.pageYOffset, scrollX: stubs.scrollX, scrollY: stubs.scrollY, // Screen position screenX: stubs.screenX, screenY: stubs.screenY, screenLeft: stubs.screenLeft, screenTop: stubs.screenTop, // Security origin, isSecureContext: stubs.isSecureContext, crossOriginIsolated: stubs.crossOriginIsolated, originAgentCluster: stubs.originAgentCluster, // Chrome object chrome: chromeProps, // Caches caches: { open: () => Promise.resolve({ match: () => Promise.resolve(undefined), matchAll: () => Promise.resolve([]), add: () => Promise.resolve(), addAll: () => Promise.resolve(), put: () => Promise.resolve(), delete: () => Promise.resolve(false), keys: () => Promise.resolve([]), }), match: () => Promise.resolve(undefined), has: () => Promise.resolve(false), delete: () => Promise.resolve(false), keys: () => Promise.resolve([]), }, // IndexedDB indexedDB: createIndexedDB(), // Scheduler scheduler: { postTask: (cb) => Promise.resolve(cb()), }, // Speech speechSynthesis: { pending: false, speaking: false, paused: false, getVoices: () => [], speak: () => {}, cancel: () => {}, pause: () => {}, resume: () => {}, addEventListener: () => {}, removeEventListener: () => {}, }, // CSS CSS: { supports: () => true, escape: (str) => str, px: (n) => `${n}px`, em: (n) => `${n}em`, rem: (n) => `${n}rem`, vh: (n) => `${n}vh`, vw: (n) => `${n}vw`, percent: (n) => `${n}%`, }, // Match media matchMedia(query) { const matches = query.includes('prefers-color-scheme: light') || query.includes('(min-width:') || query.includes('screen'); return { matches, media: query, onchange: null, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {}, }; }, // Computed style getComputedStyle(element, pseudo) { return new Proxy({}, { get(target, prop) { if (prop === 'getPropertyValue') return () => ''; if (prop === 'length') return 0; if (prop === 'cssText') return ''; return ''; } }); }, // Scroll methods scroll() {}, scrollTo() {}, scrollBy() {}, // Focus focus() {}, blur() {}, // Print print() {}, // Alerts alert() {}, confirm() { return false; }, prompt() { return null; }, // Open/Close open() { return null; }, close() {}, stop() {}, // Animation requestAnimationFrame(cb) { return setTimeout(() => cb(performance.now()), 16); }, cancelAnimationFrame(id) { clearTimeout(id); }, requestIdleCallback(cb) { return setTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50, }), 1); }, cancelIdleCallback(id) { clearTimeout(id); }, // Timers setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout, setInterval: globalThis.setInterval, clearInterval: globalThis.clearInterval, queueMicrotask: globalThis.queueMicrotask, // Encoding btoa(str) { return Buffer.from(str, 'binary').toString('base64'); }, atob(str) { return Buffer.from(str, 'base64').toString('binary'); }, // Fetch API fetch: globalThis.fetch, Request: globalThis.Request, Response: globalThis.Response, Headers: globalThis.Headers, // URL URL: globalThis.URL, URLSearchParams: globalThis.URLSearchParams, // Events Event: globalThis.Event || class Event { constructor(type, options = {}) { this.type = type; this.bubbles = options.bubbles || false; this.cancelable = options.cancelable || false; this.composed = options.composed || false; this.defaultPrevented = false; this.timeStamp = Date.now(); } preventDefault() { this.defaultPrevented = true; } stopPropagation() {} stopImmediatePropagation() {} }, CustomEvent: globalThis.CustomEvent || class CustomEvent extends Event { constructor(type, options = {}) { super(type, options); this.detail = options.detail || null; } }, MessageEvent: class MessageEvent { constructor(type, options = {}) { this.type = type; this.data = options.data; this.origin = options.origin || ''; this.lastEventId = options.lastEventId || ''; this.source = options.source || null; this.ports = options.ports || []; } }, // Event listener management addEventListener(type, listener, options) { if (!eventListeners.has(type)) { eventListeners.set(type, []); } eventListeners.get(type).push(listener); }, removeEventListener(type, listener, options) { const listeners = eventListeners.get(type); if (listeners) { const idx = listeners.indexOf(listener); if (idx > -1) listeners.splice(idx, 1); } }, dispatchEvent(event) { const listeners = eventListeners.get(event.type); if (listeners) { listeners.forEach(fn => fn(event)); } return true; }, // Post message postMessage(data, targetOrigin, transfer) {}, // Workers Worker: class Worker { constructor(url) { this.onmessage = null; this.onerror = null; } postMessage() {} terminate() {} addEventListener() {} removeEventListener() {} }, SharedWorker: undefined, // Blob & File Blob: globalThis.Blob, File: globalThis.File || class File extends Blob { constructor(bits, name, options = {}) { super(bits, options); this.name = name; this.lastModified = options.lastModified || Date.now(); } }, FileReader: class FileReader { readAsText() { this.onload?.({ target: { result: '' } }); } readAsDataURL() { this.onload?.({ target: { result: 'data:,' } }); } readAsArrayBuffer() { this.onload?.({ target: { result: new ArrayBuffer(0) } }); } readAsBinaryString() { this.onload?.({ target: { result: '' } }); } abort() {} }, // ArrayBuffer & TypedArrays ArrayBuffer: globalThis.ArrayBuffer, SharedArrayBuffer: globalThis.SharedArrayBuffer, Uint8Array: globalThis.Uint8Array, Uint16Array: globalThis.Uint16Array, Uint32Array: globalThis.Uint32Array, Int8Array: globalThis.Int8Array, Int16Array: globalThis.Int16Array, Int32Array: globalThis.Int32Array, Float32Array: globalThis.Float32Array, Float64Array: globalThis.Float64Array, Uint8ClampedArray: globalThis.Uint8ClampedArray, BigInt64Array: globalThis.BigInt64Array, BigUint64Array: globalThis.BigUint64Array, DataView: globalThis.DataView, // Text encoding TextEncoder: globalThis.TextEncoder, TextDecoder: globalThis.TextDecoder, // Intl Intl: globalThis.Intl, // WebAssembly WebAssembly: globalThis.WebAssembly, // Core language Object: globalThis.Object, Array: globalThis.Array, String: globalThis.String, Number: globalThis.Number, Boolean: globalThis.Boolean, Symbol: globalThis.Symbol, BigInt: globalThis.BigInt, Math: globalThis.Math, Date: globalThis.Date, JSON: globalThis.JSON, RegExp: globalThis.RegExp, Error: globalThis.Error, TypeError: globalThis.TypeError, RangeError: globalThis.RangeError, SyntaxError: globalThis.SyntaxError, ReferenceError: globalThis.ReferenceError, EvalError: globalThis.EvalError, URIError: globalThis.URIError, AggregateError: globalThis.AggregateError, Promise: globalThis.Promise, Proxy: globalThis.Proxy, Reflect: globalThis.Reflect, Map: globalThis.Map, Set: globalThis.Set, WeakMap: globalThis.WeakMap, WeakSet: globalThis.WeakSet, WeakRef: globalThis.WeakRef, FinalizationRegistry: globalThis.FinalizationRegistry, // Functions Function: globalThis.Function, eval: globalThis.eval, isNaN: globalThis.isNaN, isFinite: globalThis.isFinite, parseFloat: globalThis.parseFloat, parseInt: globalThis.parseInt, decodeURI: globalThis.decodeURI, decodeURIComponent: globalThis.decodeURIComponent, encodeURI: globalThis.encodeURI, encodeURIComponent: globalThis.encodeURIComponent, // Console console: globalThis.console, // Undefined/NaN/Infinity undefined: undefined, NaN: NaN, Infinity: Infinity, }; // Self-references win.window = win; win.self = win; win.top = win; win.parent = win; win.globalThis = win; // Connect document to window document.defaultView = win; return win; } function createLocation(url) { const parsed = new URL(url); return { href: parsed.href, protocol: parsed.protocol, host: parsed.host, hostname: parsed.hostname, port: parsed.port, pathname: parsed.pathname, search: parsed.search, hash: parsed.hash, origin: parsed.origin, ancestorOrigins: { length: 0, item: () => null, contains: () => false, }, assign() {}, replace() {}, reload() {}, toString() { return this.href; }, }; } function createIndexedDB() { return { open() { return { result: null, error: null, readyState: 'done', onsuccess: null, onerror: null, onupgradeneeded: null, onblocked: null, }; }, deleteDatabase() { return { result: undefined, error: null, readyState: 'done', onsuccess: null, onerror: null, onblocked: null, }; }, databases() { return Promise.resolve([]); }, cmp() { return 0; }, }; }