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.
This commit is contained in:
ldm0
2026-09-29 16:50:19 +08:00
parent 83ab7fa4bf
commit 76bfdfd682
5 changed files with 174 additions and 0 deletions
@@ -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,
@@ -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 = '<body>same origin</body>';
(document.body || document.documentElement || document).append(externalFrame);
globalThis.retainedExternalWindow = externalFrame.contentWindow;
void retainedExternalWindow.external;
externalFrame.src = 'data:text/html,<body>cross origin</body>';
"#,
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"
);
}
@@ -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;
@@ -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();
+45
View File
@@ -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';
})()