Skip to main content

headless_engine/js/
context.rs

1use crate::network::fingerprint::{DeviceProfile, Fingerprint};
2use anyhow::Result;
3use boa_engine::{Context, Source};
4
5pub struct JsRuntime {
6    context: Context<'static>,
7}
8
9impl JsRuntime {
10    pub fn new() -> Result<Self> {
11        let default_fp = Fingerprint::for_profile(DeviceProfile::ChromeWindows);
12        Self::with_fingerprint(&default_fp)
13    }
14
15    pub fn with_fingerprint(fp: &Fingerprint) -> Result<Self> {
16        let mut context = Context::default();
17
18        let max_touch_points = if fp.is_mobile { 5 } else { 0 };
19        let dpr = if fp.is_mobile { "3.0" } else { "1.0" };
20        let mobile_bool = if fp.is_mobile { "true" } else { "false" };
21
22        let (gpu_vendor, gpu_renderer) = match fp.profile {
23            DeviceProfile::ChromeWindows => (
24                "Google Inc. (NVIDIA)",
25                "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)",
26            ),
27            DeviceProfile::ChromeLinux => (
28                "Google Inc. (Intel)",
29                "ANGLE (Intel, Mesa Intel(R) UHD Graphics 620 (KBL GT2), OpenGL 4.6)",
30            ),
31            DeviceProfile::SafariMac => ("Apple Inc.", "Apple M2 Pro"),
32            DeviceProfile::SafariIos => ("Apple Inc.", "Apple A17 Pro GPU"),
33            DeviceProfile::ChromeAndroid => ("ARM", "Mali-G715-Immortalis MC11"),
34        };
35
36        let init_script = format!(
37            r###"
38            // 1. Core Window & Global References
39            globalThis.window = globalThis;
40            globalThis.self = globalThis;
41            globalThis.top = globalThis;
42            globalThis.parent = globalThis;
43            globalThis.devicePixelRatio = {dpr};
44
45            // 2. Storage APIs (localStorage & sessionStorage)
46            function createStorageMock() {{
47                const store = new Map();
48                return {{
49                    getItem: function(k) {{ return store.has(String(k)) ? store.get(String(k)) : null; }},
50                    setItem: function(k, v) {{ store.set(String(k), String(v)); }},
51                    removeItem: function(k) {{ store.delete(String(k)); }},
52                    clear: function() {{ store.clear(); }},
53                    key: function(i) {{ const keys = Array.from(store.keys()); return keys[i] || null; }},
54                    get length() {{ return store.size; }}
55                }};
56            }}
57            globalThis.localStorage = createStorageMock();
58            globalThis.sessionStorage = createStorageMock();
59            globalThis.window.localStorage = globalThis.localStorage;
60            globalThis.window.sessionStorage = globalThis.sessionStorage;
61
62            // 3. IndexedDB API
63            globalThis.indexedDB = {{
64                open: function(name, ver) {{
65                    return {{
66                        result: {{ name: name, version: ver || 1, objectStoreNames: [] }},
67                        error: null,
68                        readyState: "done",
69                        onsuccess: null,
70                        onerror: null,
71                        onupgradeneeded: null
72                    }};
73                }},
74                databases: function() {{ return Promise.resolve([]); }},
75                deleteDatabase: function() {{ return {{ onsuccess: null, onerror: null }}; }},
76                cmp: function(a, b) {{ return a < b ? -1 : (a > b ? 1 : 0); }}
77            }};
78            globalThis.window.indexedDB = globalThis.indexedDB;
79
80            // 4. Deep Navigator Stealth Emulation
81            const pluginArray = [
82                {{
83                    name: "Chrome PDF Plugin",
84                    filename: "internal-pdf-viewer",
85                    description: "Portable Document Format",
86                    length: 1,
87                    0: {{
88                        type: "application/x-google-chrome-pdf",
89                        suffixes: "pdf",
90                        description: "Portable Document Format",
91                        enabledPlugin: null
92                    }}
93                }},
94                {{
95                    name: "Chrome PDF Viewer",
96                    filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai",
97                    description: "",
98                    length: 1,
99                    0: {{
100                        type: "application/pdf",
101                        suffixes: "pdf",
102                        description: "",
103                        enabledPlugin: null
104                    }}
105                }},
106                {{
107                    name: "Native Client",
108                    filename: "internal-nacl-plugin",
109                    description: "",
110                    length: 2,
111                    0: {{
112                        type: "application/x-nacl",
113                        suffixes: "",
114                        description: "Native Client Executable",
115                        enabledPlugin: null
116                    }},
117                    1: {{
118                        type: "application/x-pnacl",
119                        suffixes: "",
120                        description: "Portable Native Client Executable",
121                        enabledPlugin: null
122                    }}
123                }}
124            ];
125            pluginArray.item = function(index) {{ return this[index] || null; }};
126            pluginArray.namedItem = function(name) {{
127                for (let i = 0; i < this.length; i++) {{
128                    if (this[i].name === name) return this[i];
129                }}
130                return null;
131            }};
132            pluginArray.refresh = function() {{}};
133
134            const mimeTypeArray = [
135                {{
136                    type: "application/pdf",
137                    suffixes: "pdf",
138                    description: "",
139                    enabledPlugin: pluginArray[1]
140                }},
141                {{
142                    type: "application/x-google-chrome-pdf",
143                    suffixes: "pdf",
144                    description: "Portable Document Format",
145                    enabledPlugin: pluginArray[0]
146                }},
147                {{
148                    type: "application/x-nacl",
149                    suffixes: "",
150                    description: "Native Client Executable",
151                    enabledPlugin: pluginArray[2]
152                }},
153                {{
154                    type: "application/x-pnacl",
155                    suffixes: "",
156                    description: "Portable Native Client Executable",
157                    enabledPlugin: pluginArray[2]
158                }}
159            ];
160            mimeTypeArray.item = function(index) {{ return this[index] || null; }};
161            mimeTypeArray.namedItem = function(type) {{
162                for (let i = 0; i < this.length; i++) {{
163                    if (this[i].type === type) return this[i];
164                }}
165                return null;
166            }};
167
168            globalThis.navigator = {{
169                userAgent: "{ua}",
170                appVersion: "{ua}",
171                platform: "{platform}",
172                appName: "Netscape",
173                appCodeName: "Mozilla",
174                language: "en-US",
175                languages: ["en-US", "en"],
176                webdriver: false,
177                cookieEnabled: true,
178                hardwareConcurrency: 8,
179                deviceMemory: 8,
180                maxTouchPoints: {touch},
181                vendor: "Google Inc.",
182                vendorSub: "",
183                product: "Gecko",
184                productSub: "20030107",
185                plugins: pluginArray,
186                mimeTypes: mimeTypeArray,
187                doNotTrack: null,
188                connection: {{
189                    downlink: 10,
190                    effectiveType: "4g",
191                    rtt: 50,
192                    saveData: false,
193                    onchange: null
194                }},
195                mediaDevices: {{
196                    enumerateDevices: function() {{
197                        return Promise.resolve([
198                            {{ deviceId: "default", kind: "audioinput", label: "Default - Microphone (Realtek Audio)", groupId: "audio-group-1" }},
199                            {{ deviceId: "default", kind: "audiooutput", label: "Default - Speakers (Realtek Audio)", groupId: "audio-group-1" }},
200                            {{ deviceId: "cam-01", kind: "videoinput", label: "Integrated Camera (HD Webcam)", groupId: "video-group-1" }}
201                        ]);
202                    }},
203                    getUserMedia: function() {{ return Promise.reject(new Error("Permission denied")); }},
204                    getDisplayMedia: function() {{ return Promise.reject(new Error("Permission denied")); }}
205                }},
206                getBattery: function() {{
207                    return Promise.resolve({{
208                        charging: true,
209                        chargingTime: 0,
210                        dischargingTime: Infinity,
211                        level: 1.0,
212                        onchargingchange: null,
213                        onlevelchange: null
214                    }});
215                }},
216                serviceWorker: {{
217                    controller: null,
218                    ready: Promise.resolve({{ active: null, scope: "/" }}),
219                    register: function() {{ return Promise.resolve(); }},
220                    getRegistration: function() {{ return Promise.resolve(null); }},
221                    getRegistrations: function() {{ return Promise.resolve([]); }}
222                }},
223                credentials: {{
224                    get: function() {{ return Promise.resolve(null); }},
225                    create: function() {{ return Promise.resolve(null); }},
226                    store: function() {{ return Promise.resolve(); }},
227                    preventSilentAccess: function() {{ return Promise.resolve(); }}
228                }},
229                userAgentData: {{
230                    brands: [
231                        {{ brand: "Not(A:Brand", version: "99" }},
232                        {{ brand: "Google Chrome", version: "133" }},
233                        {{ brand: "Chromium", version: "133" }}
234                    ],
235                    mobile: {is_mobile},
236                    platform: "{platform}",
237                    getHighEntropyValues: function(hints) {{
238                        return Promise.resolve({{
239                            architecture: "x86",
240                            bitness: "64",
241                            brands: [
242                                {{ brand: "Not(A:Brand", version: "99" }},
243                                {{ brand: "Google Chrome", version: "133" }},
244                                {{ brand: "Chromium", version: "133" }}
245                            ],
246                            mobile: {is_mobile},
247                            model: "",
248                            platform: "{platform}",
249                            platformVersion: "15.0.0",
250                            uaFullVersion: "133.0.6943.127"
251                        }});
252                    }}
253                }},
254                permissions: {{
255                    query: function(param) {{
256                        return Promise.resolve({{
257                            state: "default",
258                            onchange: null
259                        }});
260                    }}
261                }}
262            }};
263
264            // 5. Complete window.chrome Emulation
265            globalThis.window.chrome = {{
266                app: {{
267                    isInstalled: false,
268                    InstallState: {{
269                        DISABLED: "disabled",
270                        INSTALLED: "installed",
271                        NOT_INSTALLED: "not_installed"
272                    }},
273                    RunningState: {{
274                        CANNOT_RUN: "cannot_run",
275                        READY_TO_RUN: "ready_to_run",
276                        RUNNING: "running"
277                    }},
278                    getDetails: function() {{ return null; }},
279                    getIsInstalled: function() {{ return false; }},
280                    runningState: function() {{ return "cannot_run"; }}
281                }},
282                runtime: {{
283                    OnInstalledReason: {{
284                        CHROME_UPDATE: "chrome_update",
285                        INSTALL: "install",
286                        SHARED_MODULE_UPDATE: "shared_module_update",
287                        UPDATE: "update"
288                    }},
289                    OnRestartRequiredReason: {{
290                        APP_UPDATE: "app_update",
291                        OS_UPDATE: "os_update",
292                        PERIODIC: "periodic"
293                    }},
294                    PlatformArch: {{
295                        ARM: "arm",
296                        ARM64: "arm64",
297                        MIPS: "mips",
298                        MIPS64: "mips64",
299                        X86_32: "x86-32",
300                        X86_64: "x86-64"
301                    }},
302                    PlatformNaclArch: {{
303                        ARM: "arm",
304                        MIPS: "mips",
305                        MIPS64: "mips64",
306                        X86_32: "x86-32",
307                        X86_64: "x86-64"
308                    }},
309                    PlatformOs: {{
310                        ANDROID: "android",
311                        CROS: "cros",
312                        LINUX: "linux",
313                        MAC: "mac",
314                        OPENBSD: "openbsd",
315                        WIN: "win"
316                    }},
317                    RequestUpdateCheckStatus: {{
318                        NO_UPDATE: "no_update",
319                        THROTTLED: "throttled",
320                        UPDATE_AVAILABLE: "update_available"
321                    }},
322                    connect: function() {{}},
323                    sendMessage: function() {{}}
324                }},
325                csi: function() {{
326                    return {{
327                        onloadT: Date.now(),
328                        pageT: 142.5,
329                        startE: Date.now() - 150,
330                        tran: 15
331                    }};
332                }},
333                loadTimes: function() {{
334                    return {{
335                        commitLoadTime: Date.now() / 1000 - 0.1,
336                        connectionInfo: "h2",
337                        finishDocumentLoadTime: Date.now() / 1000,
338                        finishLoadTime: Date.now() / 1000,
339                        firstPaintAfterLoadTime: 0,
340                        firstPaintTime: Date.now() / 1000 - 0.05,
341                        navigationType: "Other",
342                        npnNegotiatedProtocol: "h2",
343                        requestTime: Date.now() / 1000 - 0.15,
344                        startLoadTime: Date.now() / 1000 - 0.15,
345                        wasAlternateProtocolAvailable: false,
346                        wasFetchedViaSpdy: true,
347                        wasNpnNegotiated: true
348                    }};
349                }}
350            }};
351
352            // 6. Screen and Display Metrics
353            globalThis.screen = {{
354                width: {sw},
355                height: {sh},
356                availWidth: {sw},
357                availHeight: {sh},
358                colorDepth: 24,
359                pixelDepth: 24,
360                availLeft: 0,
361                availTop: 0,
362                orientation: {{
363                    angle: 0,
364                    type: "landscape-primary",
365                    onchange: null
366                }}
367            }};
368            globalThis.outerWidth = {sw};
369            globalThis.outerHeight = {sh};
370            globalThis.innerWidth = {sw};
371            globalThis.innerHeight = {sh};
372
373            // 7. Notification API
374            globalThis.Notification = {{
375                permission: "default",
376                maxActions: 2,
377                requestPermission: function() {{
378                    return Promise.resolve("default");
379                }}
380            }};
381
382            // 8. WebGL Vendor & Renderer Spoofing
383            globalThis.WebGLRenderingContext = function() {{}};
384            globalThis.WebGLRenderingContext.prototype = {{
385                getParameter: function(param) {{
386                    if (param === 37445) return "{gpu_vendor}";
387                    if (param === 37446) return "{gpu_renderer}";
388                    if (param === 7936) return "WebKit";
389                    if (param === 7937) return "WebKit WebGL";
390                    return null;
391                }},
392                getSupportedExtensions: function() {{
393                    return [
394                        "ANGLE_instanced_arrays",
395                        "EXT_blend_minmax",
396                        "EXT_color_buffer_half_float",
397                        "EXT_float_blend",
398                        "EXT_frag_depth",
399                        "EXT_shader_texture_lod",
400                        "EXT_sRGB",
401                        "EXT_texture_compression_bptc",
402                        "EXT_texture_compression_rgtc",
403                        "EXT_texture_filter_anisotropic",
404                        "OES_element_index_uint",
405                        "OES_fbo_render_mipmap",
406                        "OES_standard_derivatives",
407                        "OES_texture_float",
408                        "OES_texture_float_linear",
409                        "OES_texture_half_float",
410                        "OES_texture_half_float_linear",
411                        "OES_vertex_array_object",
412                        "WEBGL_color_buffer_float",
413                        "WEBGL_compressed_texture_s3tc",
414                        "WEBGL_compressed_texture_s3tc_srgb",
415                        "WEBGL_debug_renderer_info",
416                        "WEBGL_debug_shaders",
417                        "WEBGL_depth_texture",
418                        "WEBGL_draw_buffers",
419                        "WEBGL_lose_context",
420                        "WEBGL_multi_draw"
421                    ];
422                }},
423                getExtension: function(name) {{
424                    if (name === "WEBGL_debug_renderer_info") {{
425                        return {{
426                            UNMASKED_VENDOR_WEBGL: 37445,
427                            UNMASKED_RENDERER_WEBGL: 37446
428                        }};
429                    }}
430                    return {{}};
431                }}
432            }};
433            globalThis.WebGL2RenderingContext = globalThis.WebGLRenderingContext;
434
435            // 9. Web Audio API
436            function AudioContextMock() {{
437                return {{
438                    state: "running",
439                    sampleRate: 44100,
440                    currentTime: 0.1,
441                    destination: {{ maxChannelCount: 2, channelCount: 2, channelCountMode: "explicit" }},
442                    createOscillator: function() {{
443                        return {{
444                            type: "sine",
445                            frequency: {{ value: 440, setValueAtTime: function() {{}} }},
446                            connect: function() {{}},
447                            start: function() {{}},
448                            stop: function() {{}}
449                        }};
450                    }},
451                    createGain: function() {{
452                        return {{ gain: {{ value: 1.0, setValueAtTime: function() {{}} }}, connect: function() {{}} }};
453                    }},
454                    createDynamicsCompressor: function() {{
455                        return {{
456                            threshold: {{ value: -24 }},
457                            knee: {{ value: 30 }},
458                            ratio: {{ value: 12 }},
459                            reduction: -10,
460                            attack: {{ value: 0.003 }},
461                            release: {{ value: 0.25 }},
462                            connect: function() {{}}
463                        }};
464                    }},
465                    createBufferSource: function() {{ return {{ connect: function() {{}}, start: function() {{}}, stop: function() {{}} }}; }},
466                    createAnalyser: function() {{
467                        return {{
468                            fftSize: 2048,
469                            frequencyBinCount: 1024,
470                            minDecibels: -100,
471                            maxDecibels: -30,
472                            smoothingTimeConstant: 0.8,
473                            getByteFrequencyData: function(arr) {{ if (arr && arr.fill) arr.fill(128); }},
474                            getFloatFrequencyData: function(arr) {{ if (arr && arr.fill) arr.fill(-50.0); }}
475                        }};
476                    }}
477                }};
478            }}
479            globalThis.AudioContext = AudioContextMock;
480            globalThis.webkitAudioContext = AudioContextMock;
481            globalThis.OfflineAudioContext = function(ch, len, sr) {{
482                const ctx = AudioContextMock();
483                ctx.startRendering = function() {{
484                    return Promise.resolve({{
485                        length: len,
486                        duration: len / (sr || 44100),
487                        sampleRate: sr || 44100,
488                        numberOfChannels: ch || 2,
489                        getChannelData: function(c) {{
490                            const d = new Float32Array(len || 100);
491                            for (let i = 0; i < d.length; i++) {{
492                                d[i] = Math.sin(i * 0.05) * 0.5 + 0.0001 * Math.sin(i * 1.5);
493                            }}
494                            return d;
495                        }}
496                    }});
497                }};
498                return ctx;
499            }};
500
501            // 10. Performance API
502            const _startTime = Date.now() - 320;
503            globalThis.performance = {{
504                now: function() {{ return Date.now() - _startTime; }},
505                timeOrigin: _startTime,
506                timing: {{
507                    navigationStart: _startTime,
508                    unloadEventStart: 0,
509                    unloadEventEnd: 0,
510                    redirectStart: 0,
511                    redirectEnd: 0,
512                    fetchStart: _startTime + 5,
513                    domainLookupStart: _startTime + 12,
514                    domainLookupEnd: _startTime + 25,
515                    connectStart: _startTime + 25,
516                    connectEnd: _startTime + 75,
517                    secureConnectionStart: _startTime + 40,
518                    requestStart: _startTime + 76,
519                    responseStart: _startTime + 140,
520                    responseEnd: _startTime + 185,
521                    domLoading: _startTime + 190,
522                    domInteractive: _startTime + 280,
523                    domContentLoadedEventStart: _startTime + 290,
524                    domContentLoadedEventEnd: _startTime + 295,
525                    domComplete: _startTime + 310,
526                    loadEventStart: _startTime + 315,
527                    loadEventEnd: _startTime + 320
528                }},
529                navigation: {{
530                    type: 0,
531                    redirectCount: 0
532                }},
533                memory: {{
534                    jsHeapSizeLimit: 4294705152,
535                    totalJSHeapSize: 58410652,
536                    usedJSHeapSize: 42892110
537                }},
538                getEntriesByType: function(type) {{
539                    if (type === "navigation") {{
540                        return [{{
541                            name: globalThis.location ? globalThis.location.href : "https://example.com",
542                            entryType: "navigation",
543                            startTime: 0,
544                            duration: 320,
545                            initiatorType: "navigation",
546                            nextHopProtocol: "h2",
547                            renderBlockingStatus: "non-blocking",
548                            responseStatus: 200
549                        }}];
550                    }}
551                    return [];
552                }},
553                getEntriesByName: function() {{ return []; }},
554                getEntries: function() {{ return []; }}
555            }};
556            globalThis.window.performance = globalThis.performance;
557
558            // 11. CSS Media & Animation APIs
559            globalThis.matchMedia = function(query) {{
560                return {{
561                    matches: query.includes("prefers-color-scheme") || query.includes("screen"),
562                    media: query,
563                    onchange: null,
564                    addListener: function() {{}},
565                    removeListener: function() {{}},
566                    addEventListener: function() {{}},
567                    removeEventListener: function() {{}},
568                    dispatchEvent: function() {{ return false; }}
569                }};
570            }};
571            globalThis.window.matchMedia = globalThis.matchMedia;
572            globalThis.requestAnimationFrame = function(cb) {{ return setTimeout(cb, 16); }};
573            globalThis.cancelAnimationFrame = function(id) {{ clearTimeout(id); }};
574
575            // 12. Document & Location Defaults
576            globalThis.document = {{
577                title: "",
578                cookie: "",
579                referrer: "",
580                readyState: "complete",
581                characterSet: "UTF-8",
582                compatMode: "CSS1Compat",
583                location: {{
584                    href: ""
585                }},
586                getElementById: function(id) {{ return null; }},
587                getElementsByTagName: function(tag) {{ return []; }},
588                querySelector: function(sel) {{ return null; }},
589                querySelectorAll: function(sel) {{ return []; }},
590                createElement: function(tag) {{
591                    const tagUpper = String(tag).toUpperCase();
592                    if (tagUpper === "CANVAS") {{
593                        return {{
594                            tagName: "CANVAS",
595                            width: 300,
596                            height: 150,
597                            getContext: function(type) {{
598                                if (type === "2d") {{
599                                    return {{
600                                        fillStyle: "#000000",
601                                        strokeStyle: "#000000",
602                                        font: "10px sans_serif",
603                                        fillRect: function() {{}},
604                                        strokeRect: function() {{}},
605                                        clearRect: function() {{}},
606                                        beginPath: function() {{}},
607                                        closePath: function() {{}},
608                                        moveTo: function() {{}},
609                                        lineTo: function() {{}},
610                                        arc: function() {{}},
611                                        fill: function() {{}},
612                                        stroke: function() {{}},
613                                        fillText: function() {{}},
614                                        strokeText: function() {{}},
615                                        measureText: function(text) {{
616                                            return {{
617                                                width: String(text).length * 7.5 + 0.02,
618                                                actualBoundingBoxAscent: 8,
619                                                actualBoundingBoxDescent: 2,
620                                                fontBoundingBoxAscent: 10,
621                                                fontBoundingBoxDescent: 3
622                                            }};
623                                        }},
624                                        getImageData: function(sx, sy, sw, sh) {{
625                                            const data = new Uint8ClampedArray((sw || 16) * (sh || 16) * 4);
626                                            for (let i = 0; i < data.length; i += 4) {{
627                                                data[i] = (i * 3 + 120) % 256;
628                                                data[i + 1] = (i * 7 + 80) % 256;
629                                                data[i + 2] = (i * 11 + 200) % 256;
630                                                data[i + 3] = 255;
631                                            }}
632                                            return {{ data: data, width: sw || 16, height: sh || 16 }};
633                                        }}
634                                    }};
635                                }}
636                                return new globalThis.WebGLRenderingContext();
637                            }},
638                            toDataURL: function() {{
639                                return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAAB5DiTlAAA=";
640                            }},
641                            style: {{}}
642                        }};
643                    }}
644                    return {{
645                        tagName: tagUpper,
646                        setAttribute: function() {{}},
647                        getAttribute: function() {{ return null; }},
648                        appendChild: function() {{}},
649                        style: {{}}
650                    }};
651                }}
652            }};
653
654            globalThis.location = {{
655                href: "",
656                origin: "",
657                protocol: "https:",
658                host: "",
659                hostname: "",
660                pathname: "/",
661                search: ""
662            }};
663            "###,
664            ua = fp.user_agent,
665            platform = fp.platform,
666            touch = max_touch_points,
667            dpr = dpr,
668            is_mobile = mobile_bool,
669            sw = fp.screen_width,
670            sh = fp.screen_height,
671            gpu_vendor = gpu_vendor,
672            gpu_renderer = gpu_renderer
673        );
674
675        context
676            .eval(Source::from_bytes(&init_script))
677            .map_err(|e| anyhow::anyhow!("Failed to initialize JS runtime: {}", e))?;
678
679        Ok(Self { context })
680    }
681
682    pub fn update_page_state(&mut self, url: &str, title: &str) -> Result<()> {
683        let escaped_url = url.replace('\\', "\\\\").replace('"', "\\\"");
684        let escaped_title = title.replace('\\', "\\\\").replace('"', "\\\"");
685
686        let update_script = format!(
687            r#"
688            document.title = "{}";
689            document.location.href = "{}";
690            location.href = "{}";
691            "#,
692            escaped_title, escaped_url, escaped_url
693        );
694
695        self.context
696            .eval(Source::from_bytes(&update_script))
697            .map_err(|e| anyhow::anyhow!("Failed to update JS page state: {}", e))?;
698
699        Ok(())
700    }
701
702    pub fn evaluate(&mut self, code: &str) -> Result<String> {
703        match self.context.eval(Source::from_bytes(code)) {
704            Ok(res) => Ok(res.display().to_string()),
705            Err(err) => Err(anyhow::anyhow!("JS Eval error: {}", err)),
706        }
707    }
708}