485 lines
14 KiB
JavaScript
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;
|
|
},
|
|
};
|
|
}
|