mirror of
https://github.com/lexmount/moli.git
synced 2026-09-30 16:01:42 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
})()
|
||||
Reference in New Issue
Block a user