Files
hcapEnv/src/sandbox/mocks/window.js
2026-02-20 13:16:51 +08:00

485 lines
14 KiB
JavaScript

/**
* 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;
},
};
}