frist
This commit is contained in:
484
src/sandbox/mocks/window.js
Normal file
484
src/sandbox/mocks/window.js
Normal file
@@ -0,0 +1,484 @@
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user