mirror of
https://github.com/lexmount/moli.git
synced 2026-09-30 16:01:42 +00:00
feat(navigator): expose native Scheduling capability shim
This commit is contained in:
@@ -6,6 +6,7 @@ mod media_capabilities;
|
||||
mod media_devices;
|
||||
mod navigator;
|
||||
mod navigator_subobjects;
|
||||
mod scheduling;
|
||||
mod screen;
|
||||
mod visual_viewport;
|
||||
mod window_state;
|
||||
|
||||
@@ -30,6 +30,7 @@ use super::media_capabilities::{
|
||||
};
|
||||
use super::media_devices::{build_media_devices_object, install_media_devices_template_bindings};
|
||||
use super::navigator_subobjects::{NavigatorSubobject, ensure_navigator_subobject};
|
||||
use super::scheduling::{build_scheduling_object, install_scheduling_template_bindings};
|
||||
use crate::document_runtime::DomHandle;
|
||||
use crate::native_bridge::OwnerDispatchScope;
|
||||
use crate::util::{
|
||||
@@ -74,6 +75,7 @@ const NAVIGATOR_RUNTIME_DATA_KEYS: &[&str] = &[
|
||||
"storageBuckets",
|
||||
"geolocation",
|
||||
"mediaCapabilities",
|
||||
"scheduling",
|
||||
];
|
||||
const WORKER_NAVIGATOR_INSTALLED_SLOT: &str = "__moliWorkerNavigatorInstalled";
|
||||
const WORKER_NAVIGATOR_MATERIALIZING_SLOT: &str = "__moliWorkerNavigatorMaterializing";
|
||||
@@ -193,6 +195,8 @@ struct NavigatorRuntimeDataPrototypeDeclaration {
|
||||
geolocation: (),
|
||||
#[webapi(accessor_property, getter = navigator_runtime_data_getter_callback, data = callback_data_index_value(scope, 32))]
|
||||
media_capabilities: (),
|
||||
#[webapi(accessor_property, getter = navigator_runtime_data_getter_callback, data = callback_data_index_value(scope, 33))]
|
||||
scheduling: (),
|
||||
#[webapi(accessor_property, getter = navigator_cookie_enabled_getter_callback)]
|
||||
cookie_enabled: (),
|
||||
}
|
||||
@@ -591,6 +595,9 @@ struct WindowNavigatorBackingDeclaration<'scope, 'profile> {
|
||||
|
||||
#[webapi(data_property, enumerable)]
|
||||
media_capabilities: v8::Local<'scope, v8::Value>,
|
||||
|
||||
#[webapi(data_property, enumerable)]
|
||||
scheduling: v8::Local<'scope, v8::Value>,
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
@@ -896,6 +903,7 @@ pub(in crate::context_bootstrap) fn install_navigator_template_bindings<'s>(
|
||||
install_geolocation_template_bindings(scope, template, interface_name);
|
||||
install_navigator_collection_template_bindings(scope, template, interface_name);
|
||||
install_media_capabilities_template_bindings(scope, template, interface_name);
|
||||
install_scheduling_template_bindings(scope, template, interface_name);
|
||||
let prototype = template.prototype_template(scope);
|
||||
match interface_name {
|
||||
"MediaDevices" => install_media_devices_template_bindings(scope, template),
|
||||
@@ -1106,6 +1114,7 @@ pub(super) fn build_lazy_navigator_subobject_in_current_realm<'s>(
|
||||
|| navigator_geolocation_secure_context_available(scope, owner_child, owner_popup);
|
||||
build_media_capabilities_object(scope, secure_context, worker)?.into()
|
||||
}
|
||||
NavigatorSubobject::Scheduling => build_scheduling_object(scope)?.into(),
|
||||
};
|
||||
Ok(value)
|
||||
}
|
||||
@@ -1279,6 +1288,7 @@ fn build_window_navigator_backing_for_owner<'s>(
|
||||
storage_buckets: v8::undefined(scope).into(),
|
||||
geolocation: v8::undefined(scope).into(),
|
||||
media_capabilities: v8::undefined(scope).into(),
|
||||
scheduling: v8::undefined(scope).into(),
|
||||
}
|
||||
.bind(scope)
|
||||
.map_err(|error| anyhow!("failed to bind Navigator backing object: {error}"))
|
||||
|
||||
@@ -22,11 +22,12 @@ pub(super) enum NavigatorSubobject {
|
||||
StorageBuckets,
|
||||
Geolocation,
|
||||
MediaCapabilities,
|
||||
Scheduling,
|
||||
}
|
||||
|
||||
impl NavigatorSubobject {
|
||||
#[cfg(test)]
|
||||
pub(super) const ALL: [Self; 16] = [
|
||||
pub(super) const ALL: [Self; 17] = [
|
||||
Self::Languages,
|
||||
Self::MimeTypes,
|
||||
Self::Plugins,
|
||||
@@ -43,6 +44,7 @@ impl NavigatorSubobject {
|
||||
Self::StorageBuckets,
|
||||
Self::Geolocation,
|
||||
Self::MediaCapabilities,
|
||||
Self::Scheduling,
|
||||
];
|
||||
|
||||
pub(super) fn from_key(key: &str) -> Option<Self> {
|
||||
@@ -63,6 +65,7 @@ impl NavigatorSubobject {
|
||||
"storageBuckets" => Some(Self::StorageBuckets),
|
||||
"geolocation" => Some(Self::Geolocation),
|
||||
"mediaCapabilities" => Some(Self::MediaCapabilities),
|
||||
"scheduling" => Some(Self::Scheduling),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -85,6 +88,7 @@ impl NavigatorSubobject {
|
||||
Self::StorageBuckets => "storageBuckets",
|
||||
Self::Geolocation => "geolocation",
|
||||
Self::MediaCapabilities => "mediaCapabilities",
|
||||
Self::Scheduling => "scheduling",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
|
||||
use crate::{util::throw_type_error, web_api_interfaces, webidl};
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = web_api_interfaces::Scheduling)]
|
||||
struct SchedulingObjectDeclaration {}
|
||||
|
||||
#[derive(Default, WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::Scheduling, enumerable)]
|
||||
struct SchedulingPrototypeDeclaration {
|
||||
#[webapi(method, length = 0, callback = is_input_pending_callback)]
|
||||
is_input_pending: (),
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "IsInputPendingOptions")]
|
||||
struct IsInputPendingOptions {
|
||||
#[webidl(name = "includeContinuous", default = false)]
|
||||
_include_continuous: bool,
|
||||
}
|
||||
|
||||
pub(super) fn install_scheduling_template_bindings<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_, ()>,
|
||||
template: v8::Local<'s, v8::FunctionTemplate>,
|
||||
interface_name: &str,
|
||||
) {
|
||||
if interface_name == "Scheduling" {
|
||||
SchedulingPrototypeDeclaration::initialize_prototype_template(
|
||||
scope,
|
||||
template.prototype_template(scope),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn build_scheduling_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> Result<v8::Local<'s, v8::Object>> {
|
||||
SchedulingObjectDeclaration::new()
|
||||
.bind(scope)
|
||||
.map_err(|error| anyhow!("failed to bind Scheduling object: {error}"))
|
||||
}
|
||||
|
||||
fn is_input_pending_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
if !web_api_interfaces::Scheduling::is_instance(scope, args.this()) {
|
||||
throw_type_error(scope, "Illegal invocation");
|
||||
return;
|
||||
}
|
||||
// Even the idle compatibility profile must perform dictionary conversion:
|
||||
// includeContinuous getters and their exceptions are observable WebIDL.
|
||||
if let Err(error) = webidl::parse_dictionary::<IsInputPendingOptions>(
|
||||
scope,
|
||||
args.get(0),
|
||||
webidl::Context::argument("Scheduling.isInputPending", 1),
|
||||
) {
|
||||
webidl::throw_error(scope, &error);
|
||||
return;
|
||||
}
|
||||
// Capability shim: pending-input prediction is not connected to the owner
|
||||
// queue yet. Report the idle state, not a measurement of queued CDP input.
|
||||
rv.set_bool(false);
|
||||
}
|
||||
@@ -544,6 +544,10 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
|
||||
interface: web_api_interfaces::MediaCapabilities::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::Scheduling::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::Touch::DESCRIPTOR,
|
||||
kind: ConstructorKind::Touch,
|
||||
|
||||
@@ -42,6 +42,7 @@ mod platform_identity;
|
||||
mod pointer_lock;
|
||||
mod popup_hyperlinks;
|
||||
mod promise_rejection;
|
||||
mod scheduling;
|
||||
mod security_policy;
|
||||
mod service_worker_drain;
|
||||
mod session_description;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn navigator_scheduling_exposes_native_idle_capability_contract() {
|
||||
for url in ["https://scheduling.test/", "http://scheduling.test/"] {
|
||||
let mut vm = new_storage_test_vm(url);
|
||||
assert_eq!(
|
||||
vm.eval(include_str!(
|
||||
"../../../../tests/fixtures/navigator-scheduling.js"
|
||||
))
|
||||
.expect("Scheduling idle and WebIDL contract should hold"),
|
||||
"window-ok"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigator_scheduling_materializes_in_its_navigator_realm() {
|
||||
let mut vm = new_storage_test_vm("https://scheduling-realm.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
const frame = document.createElement('iframe');
|
||||
(document.body || document.documentElement || document).appendChild(frame);
|
||||
globalThis.__schedulingFrame = frame;
|
||||
"#,
|
||||
)
|
||||
.expect("Scheduling child setup");
|
||||
materialize_single_child_default_realm_for_test(&mut vm, "Scheduling child realm");
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const child = __schedulingFrame.contentWindow;
|
||||
const getter = Object.getOwnPropertyDescriptor(Navigator.prototype, 'scheduling').get;
|
||||
const scheduling = getter.call(child.navigator);
|
||||
return JSON.stringify({
|
||||
ownRealm: Object.getPrototypeOf(scheduling) === child.Scheduling.prototype,
|
||||
stable: scheduling === child.navigator.scheduling,
|
||||
separate: scheduling !== navigator.scheduling,
|
||||
idle: Scheduling.prototype.isInputPending.call(scheduling)
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("borrowed Scheduling getter should use the Navigator realm"),
|
||||
r#"{"ownRealm":true,"stable":true,"separate":true,"idle":false}"#
|
||||
);
|
||||
}
|
||||
@@ -621,6 +621,7 @@ fn navigator_scalar_access_does_not_materialize_unused_same_object_children() {
|
||||
"Geolocation",
|
||||
"GeolocationPositionError",
|
||||
"MediaCapabilities",
|
||||
"Scheduling",
|
||||
"Clipboard",
|
||||
"ClipboardItem",
|
||||
] {
|
||||
@@ -663,7 +664,8 @@ fn navigator_scalar_access_does_not_materialize_unused_same_object_children() {
|
||||
"mediaCapabilities",
|
||||
navigator.mediaCapabilities,
|
||||
navigator.mediaCapabilities
|
||||
]
|
||||
],
|
||||
["scheduling", navigator.scheduling, navigator.scheduling]
|
||||
];
|
||||
return JSON.stringify({
|
||||
same: pairs.every(([, first, second]) => first === second),
|
||||
@@ -685,13 +687,15 @@ fn navigator_scalar_access_does_not_materialize_unused_same_object_children() {
|
||||
Geolocation.prototype,
|
||||
mediaCapabilitiesRealm:
|
||||
Object.getPrototypeOf(navigator.mediaCapabilities) ===
|
||||
MediaCapabilities.prototype
|
||||
MediaCapabilities.prototype,
|
||||
schedulingRealm:
|
||||
Object.getPrototypeOf(navigator.scheduling) === Scheduling.prototype
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("Navigator lazy subobjects should materialize"),
|
||||
r#"{"same":true,"languagesFrozen":true,"permissionsRealm":true,"mediaDevicesRealm":true,"storageRealm":true,"clipboardRealm":true,"geolocationRealm":true,"mediaCapabilitiesRealm":true}"#
|
||||
r#"{"same":true,"languagesFrozen":true,"permissionsRealm":true,"mediaDevicesRealm":true,"storageRealm":true,"clipboardRealm":true,"geolocationRealm":true,"mediaCapabilitiesRealm":true,"schedulingRealm":true}"#
|
||||
);
|
||||
assert_eq!(
|
||||
default_navigator_subobjects(&mut vm),
|
||||
@@ -712,10 +716,12 @@ fn navigator_scalar_access_does_not_materialize_unused_same_object_children() {
|
||||
"storageBuckets",
|
||||
"geolocation",
|
||||
"mediaCapabilities",
|
||||
"scheduling",
|
||||
]
|
||||
);
|
||||
assert_eq!(constructor_materialization_count(&mut vm, "Geolocation"), 1);
|
||||
assert_eq!(constructor_materialization_count(&mut vm, "Clipboard"), 1);
|
||||
assert_eq!(constructor_materialization_count(&mut vm, "Scheduling"), 1);
|
||||
assert_eq!(
|
||||
constructor_materialization_count(&mut vm, "ClipboardItem"),
|
||||
0,
|
||||
|
||||
@@ -416,6 +416,7 @@ interfaces! {
|
||||
SVGTransform;
|
||||
SVGTransformList;
|
||||
SVGUseElement: SVGGraphicsElement;
|
||||
Scheduling;
|
||||
Screen;
|
||||
ScreenOrientation: EventTarget;
|
||||
SecurityPolicyViolationEvent: Event;
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_navigator_does_not_expose_window_scheduling() {
|
||||
ensure_v8();
|
||||
let mut handle = spawn_worker(
|
||||
format!(
|
||||
"try {{ postMessage({}); }} catch (error) {{ postMessage(String(error)); }} close();",
|
||||
include_str!("../../../../tests/fixtures/navigator-scheduling.js")
|
||||
),
|
||||
"https://scheduling-worker.test/worker.js".into(),
|
||||
);
|
||||
let message = timeout(TIMEOUT, handle.recv())
|
||||
.await
|
||||
.expect("worker Scheduling 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_compression_streams_roundtrip_all_formats() {
|
||||
ensure_v8();
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Idle/capability contract only; this does not test queued-input reporting.
|
||||
(() => {
|
||||
const assert = (condition, message) => { if (!condition) throw new Error(message); };
|
||||
if (typeof window === 'undefined') {
|
||||
assert(typeof Scheduling === 'undefined', 'Scheduling must not be exposed in workers');
|
||||
assert(!('scheduling' in navigator), 'WorkerNavigator must not expose scheduling');
|
||||
return 'worker-ok';
|
||||
}
|
||||
const scheduling = navigator.scheduling;
|
||||
assert(typeof Scheduling === 'function', 'Scheduling interface');
|
||||
assert(scheduling instanceof Scheduling, 'Scheduling brand');
|
||||
assert(Object.getPrototypeOf(scheduling) === Scheduling.prototype, 'Scheduling prototype');
|
||||
assert(Object.prototype.toString.call(scheduling) === '[object Scheduling]', 'Scheduling tag');
|
||||
assert(navigator.scheduling === scheduling, 'cached per-Navigator object');
|
||||
assert(Object.getOwnPropertyNames(scheduling).length === 0, 'no own implementation fields');
|
||||
assert(!Object.hasOwn(navigator, 'scheduling'), 'Navigator prototype accessor');
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'scheduling');
|
||||
assert(descriptor.enumerable && descriptor.configurable && !descriptor.set, 'readonly accessor flags');
|
||||
assert(descriptor.get.call(navigator) === scheduling, 'borrowed getter');
|
||||
const method = Scheduling.prototype.isInputPending;
|
||||
const methodDescriptor = Object.getOwnPropertyDescriptor(Scheduling.prototype, 'isInputPending');
|
||||
assert(methodDescriptor.enumerable && methodDescriptor.configurable && methodDescriptor.writable, 'method flags');
|
||||
assert(method.length === 0 && method.name === 'isInputPending', 'method signature');
|
||||
assert(Function.prototype.toString.call(method).includes('[native code]'), 'native method');
|
||||
const throwsTypeError = (callback, message) => {
|
||||
let caught;
|
||||
try { callback(); } catch (error) { caught = error; }
|
||||
assert(caught instanceof TypeError, message);
|
||||
};
|
||||
throwsTypeError(() => new Scheduling(), 'illegal constructor');
|
||||
throwsTypeError(() => Scheduling(), 'illegal function call');
|
||||
for (const receiver of [null, {}, Scheduling.prototype, Object.create(Scheduling.prototype), new Proxy(scheduling, {})]) {
|
||||
throwsTypeError(() => method.call(receiver), 'illegal method receiver');
|
||||
}
|
||||
throwsTypeError(() => descriptor.get.call({}), 'illegal Navigator receiver');
|
||||
for (const options of [undefined, null, {}, {includeContinuous:false}, {includeContinuous:true}, () => {}]) {
|
||||
assert(method.call(scheduling, options) === false, 'idle result');
|
||||
}
|
||||
for (const options of [1, true, 'x', Symbol('x')]) {
|
||||
throwsTypeError(() => method.call(scheduling, options), 'dictionary conversion');
|
||||
}
|
||||
const accesses = [];
|
||||
const options = new Proxy({}, {get(_target, key) { accesses.push(key); return {valueOf() { throw new Error('no coercion'); }}; }});
|
||||
assert(method.call(scheduling, options) === false, 'truthy includeContinuous');
|
||||
assert(accesses.join(',') === 'includeContinuous', 'only read the declared dictionary member');
|
||||
const sentinel = new Error('options getter');
|
||||
let caught;
|
||||
try { method.call(scheduling, {get includeContinuous() { throw sentinel; }}); } catch (error) { caught = error; }
|
||||
assert(caught === sentinel, 'preserve dictionary getter exception');
|
||||
let reads = 0;
|
||||
throwsTypeError(() => method.call({}, {get includeContinuous() { reads++; }}), 'brand precedes conversion');
|
||||
assert(reads === 0, 'invalid receiver must not read options');
|
||||
return 'window-ok';
|
||||
})()
|
||||
Reference in New Issue
Block a user