From 76bfdfd682be2a7fb0b367411c74e496c1d5a16d Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 13 Sep 2026 20:09:12 +0800 Subject: [PATCH] test(window): extend native External compatibility coverage Reuse the native External implementation now provided by main instead of registering a second interface, object factory, and Window accessor. Retain lazy-materialization diagnostics and regression coverage for intrinsic identity, cross-realm access, cross-origin denial, replacement semantics, illegal receivers, and Worker non-exposure. --- .../window_lazy_surface/diagnostics.rs | 2 + .../script_vm/tests/browser_api/external.rs | 109 ++++++++++++++++++ .../src/script_vm/tests/browser_api/mod.rs | 1 + .../src/worker/thread/tests/postmessage.rs | 17 +++ .../tests/fixtures/window-external.js | 45 ++++++++ 5 files changed, 174 insertions(+) create mode 100644 moli-renderer-v8/src/script_vm/tests/browser_api/external.rs create mode 100644 moli-renderer-v8/tests/fixtures/window-external.js diff --git a/moli-renderer-v8/src/context_bootstrap/window_lazy_surface/diagnostics.rs b/moli-renderer-v8/src/context_bootstrap/window_lazy_surface/diagnostics.rs index 715386f515..798a4c7f58 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_lazy_surface/diagnostics.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_lazy_surface/diagnostics.rs @@ -6,6 +6,7 @@ pub(crate) fn window_lazy_surface_diagnostics( ) -> WindowLazySurfaceDiagnostics { let global = scope.get_current_context().global(scope); WindowLazySurfaceDiagnostics { + external_materialized: private_object_present(scope, global, WindowLazySurface::External), navigator_materialized: private_object_present(scope, global, WindowLazySurface::Navigator), performance_materialized: private_object_present( scope, @@ -34,6 +35,7 @@ pub(crate) fn window_lazy_surface_diagnostics( #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct WindowLazySurfaceDiagnostics { + pub(crate) external_materialized: bool, pub(crate) navigator_materialized: bool, pub(crate) performance_materialized: bool, pub(crate) custom_elements_materialized: bool, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/external.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/external.rs new file mode 100644 index 0000000000..7e9aeeffb9 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/external.rs @@ -0,0 +1,109 @@ +use super::*; + +#[test] +fn window_external_exposes_native_legacy_contract() { + for url in ["https://external.test/", "http://external.test/"] { + let mut vm = new_storage_test_vm(url); + assert_eq!( + vm.eval(include_str!( + "../../../../tests/fixtures/window-external.js" + )) + .expect("External WebIDL contract"), + "window-ok" + ); + } +} + +#[test] +fn window_external_is_lazy_and_uses_intrinsic_interface_identity() { + let mut vm = new_storage_test_vm("https://external-lazy.test/"); + let materialized = |vm: &mut ScriptVm| { + vm.with_default_context_scope_and_checkpoint_for_test(|scope, _| { + Ok( + crate::context_bootstrap::window_lazy_surface_diagnostics(scope) + .external_materialized, + ) + }) + .expect("External cache state") + }; + assert!(!materialized(&mut vm)); + assert_eq!( + vm.eval( + "globalThis.externalGetter = Object.getOwnPropertyDescriptor(window, 'external').get; window.external = 1; typeof External" + ).unwrap(), + "function" + ); + assert!(!materialized(&mut vm)); + assert_eq!( + vm.eval( + r#" + const intrinsic = External; + globalThis.External = function Replacement() { throw new Error('public constructor'); }; + const object = externalGetter.call(window); + JSON.stringify([object instanceof intrinsic, externalGetter.call(window) === object, + window.external === 1, object.AddSearchProvider() === undefined]) + "# + ) + .unwrap(), + "[true,true,true,true]" + ); + assert!(materialized(&mut vm)); +} + +#[test] +fn window_external_borrowed_getter_uses_receiver_realm() { + let mut vm = new_storage_test_vm("https://external-realm.test/"); + vm.eval( + r#" + globalThis.externalFrame = document.createElement('iframe'); + (document.body || document.documentElement || document).append(externalFrame); + "#, + ) + .unwrap(); + materialize_single_child_default_realm_for_test(&mut vm, "External child realm"); + assert_eq!( + vm.eval( + r#" + const child = externalFrame.contentWindow; + const getter = Object.getOwnPropertyDescriptor(window, 'external').get; + const object = getter.call(child); + JSON.stringify([object === child.external, object !== window.external, + Object.getPrototypeOf(object) === child.External.prototype, + External.prototype.AddSearchProvider.call(object) === undefined, + child.External.prototype.IsSearchProviderInstalled.call(window.external) === undefined]) + "# + ) + .unwrap(), + "[true,true,true,true,true]" + ); +} + +#[test] +fn window_external_stays_denied_after_cross_origin_navigation() { + let mut vm = new_storage_test_vm("https://external-origin.test/"); + vm.exec( + r#" + globalThis.externalFrame = document.createElement('iframe'); + externalFrame.srcdoc = 'same origin'; + (document.body || document.documentElement || document).append(externalFrame); + globalThis.retainedExternalWindow = externalFrame.contentWindow; + void retainedExternalWindow.external; + externalFrame.src = 'data:text/html,cross origin'; + "#, + None, + ) + .unwrap(); + vm.drain_pending_child_frame_work_for_test(); + assert_eq!( + vm.eval( + r#" + (() => { + try { return typeof retainedExternalWindow.external; } + catch (error) { return error.name; } + })() + "# + ) + .unwrap(), + "SecurityError" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index b14c7f6b91..c257c01521 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -20,6 +20,7 @@ mod details; mod event_handlers; mod event_listener_options; mod events_selection_storage; +mod external; mod fontface_descriptors; mod fonts; mod gamepad; diff --git a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs index 45b483558f..56c83a057c 100644 --- a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs +++ b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs @@ -1,5 +1,22 @@ use super::*; +#[tokio::test] +async fn worker_does_not_expose_window_external() { + ensure_v8(); + let mut handle = spawn_worker( + format!( + "try {{ postMessage({}); }} catch (error) {{ postMessage(String(error)); }} close();", + include_str!("../../../../tests/fixtures/window-external.js") + ), + "https://external-worker.test/worker.js".into(), + ); + let message = timeout(TIMEOUT, handle.recv()) + .await + .expect("Worker External exposure probe should settle") + .expect("Worker should return the exposure result"); + assert_eq!(expect_post_json(message), r#""worker-ok""#); +} + #[tokio::test] async fn worker_navigator_does_not_expose_window_scheduling() { ensure_v8(); diff --git a/moli-renderer-v8/tests/fixtures/window-external.js b/moli-renderer-v8/tests/fixtures/window-external.js new file mode 100644 index 0000000000..74304adb1b --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/window-external.js @@ -0,0 +1,45 @@ +(() => { + const assert = (condition, message) => { if (!condition) throw new Error(message); }; + if (typeof window === 'undefined') { + assert(typeof External === 'undefined' && typeof external === 'undefined', 'Window-only exposure'); + return 'worker-ok'; + } + const object = window.external, prototype = External.prototype; + const descriptor = Object.getOwnPropertyDescriptor(window, 'external'); + assert(object === window.external, 'SameObject'); + assert(object instanceof External && Object.getPrototypeOf(object) === prototype, 'native interface'); + assert(Object.prototype.toString.call(object) === '[object External]', 'toStringTag'); + assert(Object.getOwnPropertyNames(object).length === 0, 'no own implementation fields'); + assert(Object.getOwnPropertyNames(prototype).join(',') === 'AddSearchProvider,IsSearchProviderInstalled,constructor', 'prototype surface'); + assert(descriptor.enumerable && descriptor.configurable && typeof descriptor.get === 'function' && typeof descriptor.set === 'function', 'replaceable accessor'); + const throwsTypeError = (callback, message) => { + let caught; + try { callback(); } catch (error) { caught = error; } + assert(caught instanceof TypeError, message); + }; + throwsTypeError(() => External(), 'illegal call'); + throwsTypeError(() => new External(), 'illegal constructor'); + throwsTypeError(() => descriptor.get.call({}), 'illegal Window receiver'); + let coercions = 0; + const extra = {[Symbol.toPrimitive]() { coercions++; return 'unused'; }}; + for (const name of ['AddSearchProvider', 'IsSearchProviderInstalled']) { + const method = prototype[name], flags = Object.getOwnPropertyDescriptor(prototype, name); + assert(method.name === name && method.length === 0, 'method signature'); + assert(flags.enumerable && flags.configurable && flags.writable, 'method descriptor'); + assert(Function.prototype.toString.call(method).includes('[native code]'), 'native method'); + assert(method.call(object, extra) === undefined, 'void no-op'); + for (const receiver of [null, {}, prototype, Object.create(prototype), Object.create(object), new Proxy(object, {})]) { + throwsTypeError(() => method.call(receiver, extra), 'illegal External receiver'); + } + } + assert(coercions === 0, 'IDL declares no arguments'); + window.external = 42; + const replaced = Object.getOwnPropertyDescriptor(window, 'external'); + assert(replaced.value === 42 && replaced.writable && replaced.enumerable && replaced.configurable, 'replacement data property'); + assert(descriptor.get.call(window) === object, 'replacement must not overwrite native cache'); + assert(delete window.external, 'deletable replacement'); + assert(typeof window.external === 'undefined', 'delete must not resurrect accessor'); + Object.defineProperty(window, 'external', descriptor); + assert(window.external === object, 'restored accessor retains SameObject'); + return 'window-ok'; +})()