diff --git a/moli-protocol/src/domains/emulation/tests/native_navigator.rs b/moli-protocol/src/domains/emulation/tests/native_navigator.rs
index 37170b3be5..2366b2ce3e 100644
--- a/moli-protocol/src/domains/emulation/tests/native_navigator.rs
+++ b/moli-protocol/src/domains/emulation/tests/native_navigator.rs
@@ -3,6 +3,8 @@ use crate::devtools_runtime::{
DevToolsDevicePixelRatioSetting, DevToolsSetViewportCommand, DevToolsViewportSetting,
};
+mod touch_feature_detection;
+
async fn setup() -> TestContext {
let mut ctx = TestContext::new();
let mut bc = BrowserContext::new("BID-1".into());
diff --git a/moli-protocol/src/domains/emulation/tests/native_navigator/touch_feature_detection.rs b/moli-protocol/src/domains/emulation/tests/native_navigator/touch_feature_detection.rs
new file mode 100644
index 0000000000..28310f1f86
--- /dev/null
+++ b/moli-protocol/src/domains/emulation/tests/native_navigator/touch_feature_detection.rs
@@ -0,0 +1,234 @@
+use super::*;
+
+const SNAPSHOT: &str = r#"function(w) {
+ const names = ['ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel'];
+ const owners = [w, w.Document.prototype, w.HTMLElement.prototype, w.SVGElement.prototype, w.MathMLElement.prototype];
+ const present = names.every(name => owners.every(owner => {
+ const d = Object.getOwnPropertyDescriptor(owner, name);
+ return d && typeof d.get === 'function' && typeof d.set === 'function' && d.enumerable && d.configurable;
+ }));
+ let legacy;
+ try {
+ const e = w.document.createEvent('TouchEvent');
+ let beforeInit;
+ try { w.document.dispatchEvent(e); } catch (error) { beforeInit = error.name; }
+ legacy = [e instanceof w.TouchEvent, e.type, e.touches, e.targetTouches, e.changedTouches, beforeInit];
+ e.initEvent('touchstart', false, false);
+ if (!w.document.dispatchEvent(e)) throw new Error('initialized event should dispatch');
+ } catch (error) { legacy = error.name; }
+ return {points: w.navigator.maxTouchPoints, present,
+ absent: names.every(name => owners.every(owner => !(name in owner))),
+ constructor: new w.TouchEvent('touchstart').touches.length === 0 && typeof w.Touch === 'function', legacy};
+}"#;
+
+fn expected(points: u32, enabled: bool) -> serde_json::Value {
+ json!({"points": points, "present": enabled, "absent": !enabled, "constructor": true,
+ "legacy": if enabled { json!([true, "", null, null, null, "InvalidStateError"]) } else { json!("NotSupportedError") }})
+}
+
+async fn command(
+ ctx: &mut TestContext,
+ method: &str,
+ params: serde_json::Value,
+) -> serde_json::Value {
+ ctx.process_async(
+ json!({"id": 88010, "sessionId": "SID-1", "method": method, "params": params}),
+ )
+ .await;
+ crate::testing::wait_until_scheduler_message(ctx, "touch capability command", |message| {
+ message["id"] == json!(88010)
+ })
+ .await;
+ let response = ctx.take_response_by_id(88010);
+ assert!(response["error"].is_null(), "{response}");
+ response["result"].clone()
+}
+
+async fn isolated_snapshot(
+ ctx: &mut TestContext,
+ frame: &serde_json::Value,
+ name: &str,
+) -> serde_json::Value {
+ let world = command(
+ ctx,
+ "Page.createIsolatedWorld",
+ json!({"frameId": frame, "worldName": name}),
+ )
+ .await;
+ let result = command(ctx, "Runtime.evaluate", json!({
+ "contextId": world["executionContextId"], "expression": format!("({SNAPSHOT})(globalThis)"), "returnByValue": true
+ })).await;
+ assert!(result["exceptionDetails"].is_null(), "{result}");
+ result["result"]["value"].clone()
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn touch_feature_detection_follows_document_lifetime_not_live_emulation() {
+ let mut ctx = setup().await;
+ for (stage, points, reload, existing_enabled, new_enabled) in [
+ ("desktop", 0, false, false, false),
+ ("enable", 5, false, false, true),
+ ("reload-enabled", 5, true, true, true),
+ ("disable", 0, false, true, false),
+ ("reload-disabled", 0, true, false, false),
+ ] {
+ if stage == "enable" || stage == "disable" {
+ command(
+ &mut ctx,
+ "Emulation.setTouchEmulationEnabled",
+ json!({"enabled": points > 0, "maxTouchPoints": 5}),
+ )
+ .await;
+ }
+ if reload {
+ ctx.install_buffered_navigation_fixture_for_session_owner(
+ url::Url::parse(&format!("https://geolocation.example/{stage}")).unwrap(),
+ "
".into(),
+ Some("SID-1"),
+ ).await;
+ assert_eq!(
+ evaluate(&mut ctx, "initialTouchHandler").await,
+ json!(existing_enabled)
+ );
+ }
+ assert_eq!(
+ evaluate(&mut ctx, &format!("({SNAPSHOT})(globalThis)")).await,
+ expected(points, existing_enabled),
+ "main: {stage}"
+ );
+ let tree = command(&mut ctx, "Page.getFrameTree", json!({})).await;
+ assert_eq!(
+ isolated_snapshot(&mut ctx, &tree["frameTree"]["frame"]["id"], stage).await,
+ expected(points, existing_enabled),
+ "isolated: {stage}"
+ );
+ let child = evaluate(&mut ctx, &format!(r#"(async () => {{
+ const frame = document.createElement('iframe');
+ frame.id = 'touch-child'; frame.srcdoc = 'child';
+ await new Promise(resolve => {{ frame.onload = resolve; document.body.appendChild(frame); }});
+ return ({SNAPSHOT})(frame.contentWindow);
+ }})()"#)).await;
+ assert_eq!(child, expected(points, new_enabled), "child: {stage}");
+ let tree = command(&mut ctx, "Page.getFrameTree", json!({})).await;
+ let child_frame = &tree["frameTree"]["childFrames"][0]["frame"]["id"];
+ assert!(child_frame.is_string(), "{tree}");
+ assert_eq!(
+ isolated_snapshot(&mut ctx, child_frame, stage).await,
+ expected(points, new_enabled),
+ "child isolated: {stage}"
+ );
+ let borrowed = evaluate(
+ &mut ctx,
+ r#"(() => {
+ const child = document.getElementById('touch-child').contentWindow;
+ const attempt = (method, receiver, realm) => {
+ try { return method.call(receiver, 'TouchEvent') instanceof realm.TouchEvent; }
+ catch (error) { return error.name; }
+ };
+ return [attempt(Document.prototype.createEvent, child.document, child),
+ attempt(child.Document.prototype.createEvent, document, window)];
+ })()"#,
+ )
+ .await;
+ assert_eq!(
+ borrowed,
+ json!([
+ if new_enabled {
+ json!(true)
+ } else {
+ json!("NotSupportedError")
+ },
+ if existing_enabled {
+ json!(true)
+ } else {
+ json!("NotSupportedError")
+ }
+ ]),
+ "receiver realm: {stage}"
+ );
+ evaluate(&mut ctx, "document.getElementById('touch-child').remove()").await;
+ }
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn touch_feature_detection_is_shared_when_child_isolated_world_is_created_first() {
+ let mut ctx = setup().await;
+ command(
+ &mut ctx,
+ "Emulation.setTouchEmulationEnabled",
+ json!({"enabled": true, "maxTouchPoints": 5}),
+ )
+ .await;
+ ctx.install_buffered_navigation_fixture_for_session_owner(
+ url::Url::parse("https://geolocation.example/touch-isolated-first").unwrap(),
+ "".into(),
+ Some("SID-1"),
+ )
+ .await;
+ let tree = command(&mut ctx, "Page.getFrameTree", json!({})).await;
+ let child = &tree["frameTree"]["childFrames"][0]["frame"]["id"];
+ assert!(child.is_string(), "{tree}");
+ assert_eq!(
+ isolated_snapshot(&mut ctx, child, "first").await,
+ expected(5, true)
+ );
+ command(
+ &mut ctx,
+ "Emulation.setTouchEmulationEnabled",
+ json!({"enabled": false}),
+ )
+ .await;
+ assert_eq!(
+ evaluate(
+ &mut ctx,
+ &format!("({SNAPSHOT})(document.getElementById('touch-child').contentWindow)")
+ )
+ .await,
+ expected(0, true)
+ );
+ assert_eq!(
+ isolated_snapshot(&mut ctx, child, "second").await,
+ expected(0, true)
+ );
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn touch_feature_detection_keeps_author_properties_and_native_event_factory() {
+ let mut ctx = setup().await;
+ evaluate(&mut ctx, r#"
+ window.ontouchstart = 'author window';
+ Object.defineProperty(SVGElement.prototype, 'ontouchstart', {value: 'author SVG', configurable: false});
+ void 0;
+ "#).await;
+ for enabled in [true, false, true] {
+ command(
+ &mut ctx,
+ "Emulation.setTouchEmulationEnabled",
+ json!({"enabled": enabled}),
+ )
+ .await;
+ assert_eq!(
+ evaluate(
+ &mut ctx,
+ "[window.ontouchstart, SVGElement.prototype.ontouchstart]"
+ )
+ .await,
+ json!(["author window", "author SVG"])
+ );
+ }
+ ctx.install_buffered_navigation_fixture_for_session_owner(
+ url::Url::parse("https://geolocation.example/touch-factory").unwrap(),
+ "touch".into(),
+ Some("SID-1"),
+ )
+ .await;
+ assert_eq!(evaluate(&mut ctx, r#"(() => {
+ const OriginalTouchEvent = TouchEvent;
+ window.TouchEvent = function() { throw new Error('author constructor must not run'); };
+ const e = document.createEvent('tOuChEvEnT');
+ let invalidReceiver;
+ try { Document.prototype.createEvent.call(Object.create(Document.prototype), 'TouchEvent'); }
+ catch (error) { invalidReceiver = error.name; }
+ return [e instanceof OriginalTouchEvent, e.touches, e.type, invalidReceiver];
+ })()"#).await, json!([true, null, "", "TypeError"]));
+}
diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs
index 8f68490297..f39df433f3 100644
--- a/moli-renderer-v8/src/context_bootstrap.rs
+++ b/moli-renderer-v8/src/context_bootstrap.rs
@@ -180,6 +180,7 @@ mod stream_objects;
mod streams;
mod style_font_template;
pub(crate) mod svg_runtime;
+pub(crate) mod touch_feature_detection;
mod touch_runtime;
mod trusted_types;
mod url_form;
diff --git a/moli-renderer-v8/src/context_bootstrap/event_document.rs b/moli-renderer-v8/src/context_bootstrap/event_document.rs
index 4dbfb46ced..2fc7c04314 100644
--- a/moli-renderer-v8/src/context_bootstrap/event_document.rs
+++ b/moli-renderer-v8/src/context_bootstrap/event_document.rs
@@ -1,4 +1,6 @@
-use super::events::{new_uninitialized_text_event, set_event_initialized};
+use super::events::{
+ new_uninitialized_text_event, new_uninitialized_touch_event, set_event_initialized,
+};
use super::*;
use crate::webidl;
use std::str::FromStr;
@@ -28,6 +30,7 @@ enum DocumentCreateEventKind {
#[strum(serialize = "uievent", serialize = "uievents")]
UiEvent,
TextEvent,
+ TouchEvent,
CompositionEvent,
FocusEvent,
HashChangeEvent,
@@ -50,6 +53,7 @@ impl DocumentCreateEventKind {
DocumentCreateEventKind::DragEvent => "Event",
DocumentCreateEventKind::UiEvent => "UIEvent",
DocumentCreateEventKind::TextEvent => "TextEvent",
+ DocumentCreateEventKind::TouchEvent => "TouchEvent",
DocumentCreateEventKind::CompositionEvent => "CompositionEvent",
DocumentCreateEventKind::FocusEvent => "FocusEvent",
DocumentCreateEventKind::HashChangeEvent => "Event",
@@ -71,6 +75,12 @@ pub(super) fn document_create_event_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
+ if crate::native_bridge::document::document_receiver_runtime_and_handle(scope, args.this())
+ .is_none()
+ {
+ throw_type_error(scope, "Illegal invocation");
+ return;
+ }
let Some(parsed) = webidl::parse_args::(scope, &args) else {
return;
};
@@ -78,6 +88,22 @@ pub(super) fn document_create_event_callback<'s>(
throw_not_supported_dom_exception(scope, "The provided event type is not supported.");
return;
};
+ if kind == DocumentCreateEventKind::TouchEvent {
+ let Some(context) =
+ crate::native_bridge::document::document_relevant_context(scope, args.this())
+ else {
+ return;
+ };
+ let scope = &mut v8::ContextScope::new(scope, context);
+ if !super::touch_feature_detection::enabled(scope) {
+ throw_not_supported_dom_exception(scope, "The provided event type is not supported.");
+ return;
+ }
+ if let Some(event) = new_uninitialized_touch_event(scope) {
+ rv.set(event.into());
+ }
+ return;
+ }
if kind == DocumentCreateEventKind::TextEvent {
match new_uninitialized_text_event(scope) {
Some(event) => rv.set(event.into()),
diff --git a/moli-renderer-v8/src/context_bootstrap/events.rs b/moli-renderer-v8/src/context_bootstrap/events.rs
index 73659b7a6c..acc334f8a2 100644
--- a/moli-renderer-v8/src/context_bootstrap/events.rs
+++ b/moli-renderer-v8/src/context_bootstrap/events.rs
@@ -94,6 +94,28 @@ pub(in crate::context_bootstrap) fn new_uninitialized_text_event<'s>(
Some(wrapper)
}
+pub(in crate::context_bootstrap) fn new_uninitialized_touch_event<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+) -> Option> {
+ let prototype =
+ super::exposed_interfaces::ensure_intrinsic_interface_prototype(scope, "TouchEvent")
+ .ok()?;
+ let event = v8::Object::new(scope);
+ if event.set_prototype(scope, prototype.into()) != Some(true) {
+ return None;
+ }
+ base::initialize_event_object(scope, event, "", false, false);
+ super::touch_runtime::initialize_uninitialized_touch_event(scope, event);
+ set_private_value(
+ scope,
+ event,
+ EVENT_SUBCLASS_KIND_SLOT,
+ v8::Integer::new(scope, EventSubclassKind::TouchEvent as i32).into(),
+ );
+ base::set_event_initialized(scope, event, false);
+ Some(event)
+}
+
pub(crate) fn construct_original_page_transition_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
event_type: &str,
diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs
index 71389fde62..60017eea66 100644
--- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs
+++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs
@@ -6,6 +6,8 @@ use super::materialize::{
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RealmDependentFinalizer {
+ Document,
+ TouchEventHandlers,
NodeMixinUnscopables,
CryptoSecureContextSurface,
XmlHttpRequestEventTargetState,
@@ -17,7 +19,7 @@ enum RealmDependentFinalizer {
const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] = &[
// The template creates @@unscopables, but its object needs a realm-local
// null prototype after materialization.
- ("Document", RealmDependentFinalizer::NodeMixinUnscopables),
+ ("Document", RealmDependentFinalizer::Document),
(
"DocumentFragment",
RealmDependentFinalizer::NodeMixinUnscopables,
@@ -33,6 +35,9 @@ const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] =
),
// These entries install private realm state, context-conditional surface,
// or constructor data whose JavaScript identity must be realm-local.
+ ("HTMLElement", RealmDependentFinalizer::TouchEventHandlers),
+ ("SVGElement", RealmDependentFinalizer::TouchEventHandlers),
+ ("MathMLElement", RealmDependentFinalizer::TouchEventHandlers),
(
"Crypto",
RealmDependentFinalizer::CryptoSecureContextSurface,
@@ -77,6 +82,21 @@ pub(super) fn finalize_materialized_interface(
return Ok(());
};
match finalizer {
+ RealmDependentFinalizer::Document => {
+ finalize_node_mixin_unscopables(scope, prototype);
+ crate::native_bridge::element::install_touch_event_handler_realm_bindings(
+ scope,
+ prototype,
+ crate::native_bridge::element::GlobalEventHandlerOwner::Document,
+ )?;
+ }
+ RealmDependentFinalizer::TouchEventHandlers => {
+ crate::native_bridge::element::install_touch_event_handler_realm_bindings(
+ scope,
+ prototype,
+ crate::native_bridge::element::GlobalEventHandlerOwner::Element,
+ )?;
+ }
RealmDependentFinalizer::NodeMixinUnscopables => {
finalize_node_mixin_unscopables(scope, prototype);
}
@@ -140,6 +160,9 @@ mod tests {
"Element",
"DocumentType",
"CharacterData",
+ "HTMLElement",
+ "SVGElement",
+ "MathMLElement",
"Crypto",
"XMLHttpRequestEventTarget",
"Notification",
diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs
index 882ede6389..cece613425 100644
--- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs
+++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs
@@ -1737,7 +1737,9 @@ pub(crate) fn finish_context_bootstrap(
scope: &mut v8::PinScope<'_, '_>,
document_runtime: &mut JsContextHost,
secure_context_url: &url::Url,
+ touch_feature_detection: bool,
) -> Result<()> {
+ super::touch_feature_detection::initialize(scope, touch_feature_detection);
super::exposed_interfaces::initialize_realm_interface_registry(
scope,
super::exposed_interfaces::RealmKind::Window,
diff --git a/moli-renderer-v8/src/context_bootstrap/touch_feature_detection.rs b/moli-renderer-v8/src/context_bootstrap/touch_feature_detection.rs
new file mode 100644
index 0000000000..6b91d456c8
--- /dev/null
+++ b/moli-renderer-v8/src/context_bootstrap/touch_feature_detection.rs
@@ -0,0 +1,24 @@
+use crate::util::{get_private_value, set_private_value};
+
+const WINDOW_TOUCH_FEATURE_DETECTION_SLOT: &str = "__moliTouchEventFeatureDetection";
+
+pub(crate) const TOUCH_EVENT_HANDLER_PROPERTIES: &[&str] =
+ &["ontouchstart", "ontouchend", "ontouchmove", "ontouchcancel"];
+
+/// Each realm receives its LocalWindow's frozen capability snapshot, not the
+/// live CDP override. Changing emulation must not mutate author properties.
+pub(crate) fn initialize(scope: &mut v8::PinScope<'_, '_>, enabled: bool) {
+ let global = scope.get_current_context().global(scope);
+ set_private_value(
+ scope,
+ global,
+ WINDOW_TOUCH_FEATURE_DETECTION_SLOT,
+ v8::Boolean::new(scope, enabled).into(),
+ );
+}
+
+pub(crate) fn enabled(scope: &mut v8::PinScope<'_, '_>) -> bool {
+ let global = scope.get_current_context().global(scope);
+ get_private_value(scope, global, WINDOW_TOUCH_FEATURE_DETECTION_SLOT)
+ .is_some_and(|value| value.boolean_value(scope))
+}
diff --git a/moli-renderer-v8/src/context_bootstrap/touch_runtime.rs b/moli-renderer-v8/src/context_bootstrap/touch_runtime.rs
index c4ef7f8137..300143dfa5 100644
--- a/moli-renderer-v8/src/context_bootstrap/touch_runtime.rs
+++ b/moli-renderer-v8/src/context_bootstrap/touch_runtime.rs
@@ -119,11 +119,11 @@ struct TouchUiEventInitDeclaration<'scope> {
#[webapi(interface = web_api_interfaces::TouchEvent)]
struct TouchEventObjectDeclaration<'scope> {
#[webapi(slot = TOUCH_EVENT_TOUCHES_SLOT)]
- touches: v8::Local<'scope, v8::Object>,
+ touches: v8::Local<'scope, v8::Value>,
#[webapi(slot = TOUCH_EVENT_TARGET_TOUCHES_SLOT)]
- target_touches: v8::Local<'scope, v8::Object>,
+ target_touches: v8::Local<'scope, v8::Value>,
#[webapi(slot = TOUCH_EVENT_CHANGED_TOUCHES_SLOT)]
- changed_touches: v8::Local<'scope, v8::Object>,
+ changed_touches: v8::Local<'scope, v8::Value>,
#[webapi(slot = TOUCH_EVENT_ALT_KEY_SLOT)]
alt_key: bool,
#[webapi(slot = TOUCH_EVENT_META_KEY_SLOT)]
@@ -425,9 +425,9 @@ pub(in crate::context_bootstrap) fn initialize_touch_event<'s>(
let ctrl_key = touch_event_init_bool(scope, init, "ctrlKey", false);
let shift_key = touch_event_init_bool(scope, init, "shiftKey", false);
TouchEventObjectDeclaration::new(
- touches,
- target_touches,
- changed_touches,
+ touches.into(),
+ target_touches.into(),
+ changed_touches.into(),
alt_key,
meta_key,
ctrl_key,
@@ -437,6 +437,17 @@ pub(in crate::context_bootstrap) fn initialize_touch_event<'s>(
.expect("TouchEvent declaration should initialize object");
}
+pub(in crate::context_bootstrap) fn initialize_uninitialized_touch_event<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ event: v8::Local<'s, v8::Object>,
+) {
+ initialize_touch_ui_event(scope, event, None);
+ let null = v8::null(scope).into();
+ TouchEventObjectDeclaration::new(null, null, null, false, false, false, false)
+ .initialize(scope, event)
+ .expect("uninitialized TouchEvent state");
+}
+
pub(in crate::context_bootstrap) fn install_touch_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
diff --git a/moli-renderer-v8/src/context_bootstrap/window_events/install.rs b/moli-renderer-v8/src/context_bootstrap/window_events/install.rs
index f44a58d8d8..efca37d5c7 100644
--- a/moli-renderer-v8/src/context_bootstrap/window_events/install.rs
+++ b/moli-renderer-v8/src/context_bootstrap/window_events/install.rs
@@ -54,6 +54,12 @@ pub(in crate::context_bootstrap) fn install_window_global_accessors<'s>(
.initialize(scope, global)
.expect("Window global event handler accessors declaration should initialize");
for name in WINDOW_EVENT_HANDLER_PROPERTIES {
+ if crate::context_bootstrap::touch_feature_detection::TOUCH_EVENT_HANDLER_PROPERTIES
+ .contains(name)
+ && !crate::context_bootstrap::touch_feature_detection::enabled(scope)
+ {
+ continue;
+ }
if matches!(
*name,
"onerror" | "onunhandledrejection" | "onrejectionhandled"
diff --git a/moli-renderer-v8/src/frame_owner_model/records.rs b/moli-renderer-v8/src/frame_owner_model/records.rs
index 409a4bcfb2..f2a14c8803 100644
--- a/moli-renderer-v8/src/frame_owner_model/records.rs
+++ b/moli-renderer-v8/src/frame_owner_model/records.rs
@@ -482,6 +482,9 @@ pub(crate) struct LocalWindowRecord {
pub(crate) realm_id: Option,
pub(crate) settings: FrameSettingsObject,
pub(crate) lifecycle: LocalWindowLifecycleState,
+ /// The first realm freezes conditional touch bindings for all worlds of
+ /// this Window. Live emulation updates do not rewrite installed bindings.
+ pub(crate) touch_feature_detection: Option,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
diff --git a/moli-renderer-v8/src/frame_owner_model/store.rs b/moli-renderer-v8/src/frame_owner_model/store.rs
index a5f5d2144f..d97b400405 100644
--- a/moli-renderer-v8/src/frame_owner_model/store.rs
+++ b/moli-renderer-v8/src/frame_owner_model/store.rs
@@ -143,6 +143,7 @@ impl FrameOwnerStore {
realm_id: Some(realm_id),
settings,
lifecycle: LocalWindowLifecycleState::Current,
+ touch_feature_detection: None,
},
);
let lifecycle_progress =
@@ -484,6 +485,7 @@ impl FrameOwnerStore {
realm_id: None,
settings,
lifecycle: LocalWindowLifecycleState::Current,
+ touch_feature_detection: None,
},
);
self.documents.insert(
@@ -3286,6 +3288,18 @@ impl FrameOwnerStore {
.is_some_and(|frame| frame.scheduler_lane_id == owner.scheduler_lane_id)
}
+ pub(crate) fn init_window_touch_feature_detection(
+ &mut self,
+ owner: FrameDocumentTaskOwner,
+ enabled: bool,
+ ) -> Option {
+ if !self.document_task_owner_is_current(owner) {
+ return None;
+ }
+ let window = self.local_windows.get_mut(&owner.local_window_id)?;
+ Some(*window.touch_feature_detection.get_or_insert(enabled))
+ }
+
pub(crate) fn current_reserved_realm_id_for_document_task_owner(
&self,
owner: FrameDocumentTaskOwner,
diff --git a/moli-renderer-v8/src/frame_owner_model/store_tests.rs b/moli-renderer-v8/src/frame_owner_model/store_tests.rs
index 029f75c705..9ebada9e01 100644
--- a/moli-renderer-v8/src/frame_owner_model/store_tests.rs
+++ b/moli-renderer-v8/src/frame_owner_model/store_tests.rs
@@ -138,6 +138,44 @@ fn finish_test_child_load_delivery(
unreachable!("frame-finish phase returns from the loop")
}
+#[test]
+fn touch_feature_detection_is_frozen_per_window_without_materializing_a_realm() {
+ let mut store = FrameOwnerStore::default();
+ store.ensure_main_frame(
+ handle(1),
+ url("https://example.test/"),
+ url("https://example.test/"),
+ "https://example.test".to_owned(),
+ policy_container(),
+ policy_context(),
+ None,
+ );
+ let first = commit_test_child_document(&mut store, handle(2), handle(3), "child", Some("main"));
+ assert_eq!(
+ store.init_window_touch_feature_detection(first, true),
+ Some(true)
+ );
+ assert_eq!(
+ store.init_window_touch_feature_detection(first, false),
+ Some(true)
+ );
+ assert_eq!(store.local_windows[&first.local_window_id].realm_id, None);
+ let second =
+ commit_test_child_document(&mut store, handle(4), handle(5), "other", Some("main"));
+ assert_eq!(
+ store.init_window_touch_feature_detection(second, false),
+ Some(false)
+ );
+ let replacement =
+ commit_test_child_document(&mut store, handle(2), handle(6), "child", Some("main"));
+ assert_ne!(replacement.local_window_id, first.local_window_id);
+ assert_eq!(store.init_window_touch_feature_detection(first, true), None);
+ assert_eq!(
+ store.init_window_touch_feature_detection(replacement, false),
+ Some(false)
+ );
+}
+
#[test]
fn main_frame_identity_uses_reserved_owner_records() {
let mut store = FrameOwnerStore::default();
diff --git a/moli-renderer-v8/src/native_bridge/context_host/navigator_overrides.rs b/moli-renderer-v8/src/native_bridge/context_host/navigator_overrides.rs
index aeca48c595..8b2fc4a728 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/navigator_overrides.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/navigator_overrides.rs
@@ -18,6 +18,19 @@ impl JsContextHost {
geolocation_changed
}
+ pub(crate) fn init_window_touch_feature_detection(
+ &mut self,
+ owner: crate::frame_owner_model::FrameDocumentTaskOwner,
+ ) -> Option {
+ let max_touch_points = self
+ .navigator_overrides
+ .max_touch_points
+ .map(f64::from)
+ .unwrap_or(moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE.max_touch_points);
+ self.frame_owner_store
+ .init_window_touch_feature_detection(owner, max_touch_points > 0.0)
+ }
+
pub(crate) fn register_geolocation_object<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs
index b3fc9286cb..4646c2a82d 100644
--- a/moli-renderer-v8/src/native_bridge/document.rs
+++ b/moli-renderer-v8/src/native_bridge/document.rs
@@ -504,7 +504,7 @@ struct DocumentFragmentCollectionQueryPrototypeDeclaration {
get_elements_by_name: (),
}
-fn document_receiver_runtime_and_handle<'s>(
+pub(crate) fn document_receiver_runtime_and_handle<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<(*mut JsContextHost, DomHandle)> {
@@ -1303,6 +1303,21 @@ pub(in crate::native_bridge) fn set_document_associated_window<'s>(
);
}
+pub(crate) fn document_relevant_context<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ document: v8::Local<'s, v8::Object>,
+) -> Option> {
+ // A shared default-world reflector can predate its child realm. Its V8
+ // allocation context is then not the Document's current associated Window.
+ if let Some(window) = get_private_value(scope, document, DOCUMENT_ASSOCIATED_WINDOW_SLOT)
+ .and_then(|value| v8::Local::::try_from(value).ok())
+ {
+ return window.get_creation_context(scope);
+ }
+ // Detached documents have no Window and retain their creation realm.
+ document.get_creation_context(scope)
+}
+
pub(crate) fn install_document_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs
index 2121bd02a5..d2966a8ff9 100644
--- a/moli-renderer-v8/src/native_bridge/element.rs
+++ b/moli-renderer-v8/src/native_bridge/element.rs
@@ -259,6 +259,7 @@ use event_handlers::install_global_event_handler_template_bindings as install_gl
pub(crate) use event_handlers::{
EventAttributeHandlerScope, GlobalEventHandlerOwner, compile_event_attribute_handler_for_owner,
initialize_parser_inserted_body_window_event_handlers,
+ install_touch_event_handler_realm_bindings,
};
use event_handlers::{
body_onerror_getter_function, body_onerror_setter_function, body_onload_getter_function,
diff --git a/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs b/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs
index 828774067e..2eb0f6e15c 100644
--- a/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs
+++ b/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs
@@ -1,4 +1,5 @@
use crate::{
+ context_bootstrap::touch_feature_detection::{self, TOUCH_EVENT_HANDLER_PROPERTIES},
document_runtime::EventTargetHandle,
util::{
context_host_ptr_from_global_bridge, node_wrapper_from_handle, throw_type_error, v8_string,
@@ -133,35 +134,10 @@ pub(crate) fn install_global_event_handler_template_bindings<'s>(
if matches!(owner, GlobalEventHandlerOwner::Document) && *name == "onstorage" {
continue;
}
- let data = v8str(scope, name).into();
- let (getter, setter) = match owner {
- GlobalEventHandlerOwner::Document => (
- v8::FunctionTemplate::builder(document_event_handler_getter_function)
- .data(data)
- .length(0)
- .build(scope),
- v8::FunctionTemplate::builder(document_event_handler_setter_function)
- .data(data)
- .length(1)
- .build(scope),
- ),
- GlobalEventHandlerOwner::Element => (
- v8::FunctionTemplate::builder(node_event_handler_getter_function)
- .data(data)
- .length(0)
- .build(scope),
- v8::FunctionTemplate::builder(node_event_handler_setter_function)
- .data(data)
- .length(1)
- .build(scope),
- ),
- };
- if let Some(function_name) = v8_string(scope, &format!("get {name}")) {
- getter.set_class_name(function_name);
- }
- if let Some(function_name) = v8_string(scope, &format!("set {name}")) {
- setter.set_class_name(function_name);
+ if TOUCH_EVENT_HANDLER_PROPERTIES.contains(name) {
+ continue;
}
+ let (getter, setter) = event_handler_templates(scope, name, owner);
prototype.set_accessor_property(
v8str(scope, name).into(),
Some(getter),
@@ -171,6 +147,71 @@ pub(crate) fn install_global_event_handler_template_bindings<'s>(
}
}
+fn event_handler_templates<'s>(
+ scope: &mut v8::PinScope<'s, '_, ()>,
+ name: &'static str,
+ owner: GlobalEventHandlerOwner,
+) -> (
+ v8::Local<'s, v8::FunctionTemplate>,
+ v8::Local<'s, v8::FunctionTemplate>,
+) {
+ let data = v8str(scope, name).into();
+ let (getter, setter) = match owner {
+ GlobalEventHandlerOwner::Document => (
+ v8::FunctionTemplate::builder(document_event_handler_getter_function)
+ .data(data)
+ .length(0)
+ .build(scope),
+ v8::FunctionTemplate::builder(document_event_handler_setter_function)
+ .data(data)
+ .length(1)
+ .build(scope),
+ ),
+ GlobalEventHandlerOwner::Element => (
+ v8::FunctionTemplate::builder(node_event_handler_getter_function)
+ .data(data)
+ .length(0)
+ .build(scope),
+ v8::FunctionTemplate::builder(node_event_handler_setter_function)
+ .data(data)
+ .length(1)
+ .build(scope),
+ ),
+ };
+ if let Some(function_name) = v8_string(scope, &format!("get {name}")) {
+ getter.set_class_name(function_name);
+ }
+ if let Some(function_name) = v8_string(scope, &format!("set {name}")) {
+ setter.set_class_name(function_name);
+ }
+ (getter, setter)
+}
+
+pub(crate) fn install_touch_event_handler_realm_bindings<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ prototype: v8::Local<'s, v8::Object>,
+ owner: GlobalEventHandlerOwner,
+) -> anyhow::Result<()> {
+ if !touch_feature_detection::enabled(scope) {
+ return Ok(());
+ }
+ for name in TOUCH_EVENT_HANDLER_PROPERTIES {
+ let (getter, setter) = event_handler_templates(scope, name, owner);
+ let getter = getter.get_function(scope).expect("touch handler getter");
+ let setter = setter.get_function(scope).expect("touch handler setter");
+ crate::definitions::define_get_set_property(
+ scope,
+ prototype,
+ v8str(scope, name).into(),
+ getter.into(),
+ setter.into(),
+ v8::PropertyAttribute::NONE,
+ name,
+ )?;
+ }
+ Ok(())
+}
+
fn event_handler_property_value_for_target<'s>(
scope: &mut v8::PinScope<'s, '_>,
runtime_ptr: *mut super::super::super::JsContextHost,
diff --git a/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs b/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs
index fd8dcb2303..ab7959bf3f 100644
--- a/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs
+++ b/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs
@@ -12,7 +12,10 @@ pub(crate) use body_window::compile_window_body_onmessageerror_attribute;
pub(super) use body_window::{
body_onmessageerror_getter_function, body_onmessageerror_setter_function,
};
-pub(crate) use generic::{GlobalEventHandlerOwner, install_global_event_handler_template_bindings};
+pub(crate) use generic::{
+ GlobalEventHandlerOwner, install_global_event_handler_template_bindings,
+ install_touch_event_handler_realm_bindings,
+};
pub(crate) use shared::{EventAttributeHandlerScope, compile_event_attribute_handler_for_owner};
fn is_body_or_frameset_element(
diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs
index d7f620759b..81028b0e75 100644
--- a/moli-renderer-v8/src/runtime/page_vm/mod.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs
@@ -4255,6 +4255,7 @@ impl PageVm {
let vm_bootstrap = ScriptVmDefaultWorldBootstrap::from_dom_host_with_resource_completion_sender_browser_context_runtime_and_document_isolate(
bootstrap_document,
env.bypass_content_security_policy,
+ &env.navigator_overrides,
post_domcontentloaded_page_task_sender,
script_event_parser_boundary_sender,
resource_completion_sender,
@@ -4385,9 +4386,6 @@ impl PageVm {
.vm_mut()
.set_emulated_media_for_bootstrap(&env.emulated_media);
page_vm.vm_mut().set_idle_override(env.idle_override);
- page_vm
- .vm_mut()
- .set_navigator_overrides(&env.navigator_overrides);
page_vm
.vm_mut()
.set_viewport_surface_for_bootstrap(env.viewport_surface);
diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs
index fd96c5724b..0985e3d935 100644
--- a/moli-renderer-v8/src/script_vm.rs
+++ b/moli-renderer-v8/src/script_vm.rs
@@ -2193,6 +2193,7 @@ impl ScriptVmDefaultWorldBootstrap {
Self::from_dom_host_with_resource_completion_sender_browser_context_runtime_and_document_isolate(
bootstrap_dom_host,
false,
+ &moli_page_types::NavigatorOverrides::default(),
page_task_tx,
page_task_parser_boundary_injection_tx,
resource_completion_tx,
@@ -2212,6 +2213,7 @@ impl ScriptVmDefaultWorldBootstrap {
pub(super) fn from_dom_host_with_resource_completion_sender_browser_context_runtime_and_document_isolate(
bootstrap_dom_host: DomHost,
bypass_content_security_policy: bool,
+ navigator_overrides: &moli_page_types::NavigatorOverrides,
page_task_tx: RuntimePageTaskSender,
page_task_parser_boundary_injection_tx: tokio::sync::mpsc::UnboundedSender,
resource_completion_tx: RendererResourceCompletionSender,
@@ -2229,7 +2231,7 @@ impl ScriptVmDefaultWorldBootstrap {
crate::service_worker_runtime::ServiceWorkerClientId,
>,
) -> std::result::Result {
- ScriptVmPageRealmBootstrap::new_from_dom_host(
+ let bootstrap = ScriptVmPageRealmBootstrap::new_from_dom_host(
bootstrap_dom_host,
bypass_content_security_policy,
page_task_tx,
@@ -2245,8 +2247,12 @@ impl ScriptVmDefaultWorldBootstrap {
main_document_commit,
top_level_storage_key,
reserved_service_worker_client_id,
- )?
- .bootstrap_default_world()
+ )?;
+ bootstrap
+ .context_host
+ .borrow_mut()
+ .set_navigator_overrides(navigator_overrides);
+ bootstrap.bootstrap_default_world()
}
fn attach_context_and_capture_baseline_globals(
diff --git a/moli-renderer-v8/src/script_vm/document_environment.rs b/moli-renderer-v8/src/script_vm/document_environment.rs
index 1a20a4e048..88f3fd5fec 100644
--- a/moli-renderer-v8/src/script_vm/document_environment.rs
+++ b/moli-renderer-v8/src/script_vm/document_environment.rs
@@ -218,15 +218,6 @@ impl ScriptVm {
self._context_host.borrow_mut().set_network_offline(offline);
}
- pub(crate) fn set_navigator_overrides(
- &mut self,
- overrides: &moli_page_types::NavigatorOverrides,
- ) {
- self._context_host
- .borrow_mut()
- .set_navigator_overrides(overrides);
- }
-
pub(crate) fn set_navigator_overrides_and_sync_surface(
&mut self,
overrides: &moli_page_types::NavigatorOverrides,
diff --git a/moli-renderer-v8/src/script_vm/post_parse.rs b/moli-renderer-v8/src/script_vm/post_parse.rs
index c09aab2f68..f96dee0267 100644
--- a/moli-renderer-v8/src/script_vm/post_parse.rs
+++ b/moli-renderer-v8/src/script_vm/post_parse.rs
@@ -316,7 +316,22 @@ impl ScriptVmContextBootstrap {
let owner = owner_context.global(scope);
crate::context_bootstrap::bind_isolated_window_history_owner(scope, global, owner);
}
- finish_context_bootstrap(scope, unsafe { &mut *host_ptr }, &secure_context_url)?;
+ let binding_owner = match mode {
+ WindowContextBootstrapMode::MainDefault => unsafe { &*host_ptr }
+ .current_main_document_task_owner()
+ .ok_or_else(|| anyhow::anyhow!("main realm requires a current document owner"))?,
+ WindowContextBootstrapMode::ChildDefault { expected_owner, .. }
+ | WindowContextBootstrapMode::Isolated { expected_owner, .. } => expected_owner,
+ };
+ let touch_feature_detection = unsafe { &mut *host_ptr }
+ .init_window_touch_feature_detection(binding_owner)
+ .ok_or_else(|| anyhow::anyhow!("touch binding owner is no longer current"))?;
+ finish_context_bootstrap(
+ scope,
+ unsafe { &mut *host_ptr },
+ &secure_context_url,
+ touch_feature_detection,
+ )?;
match mode {
WindowContextBootstrapMode::Isolated {
child_handle: Some(child_handle),
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/platform_compat.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/platform_compat.rs
index 67f717118d..9ae67a6f92 100644
--- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/platform_compat.rs
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/platform_compat.rs
@@ -400,7 +400,7 @@ fn zhihu_bot_detection_harness_fixture_matches_stable_moli_baseline() {
assert_eq!(value["screenTouch"]["orientationType"], "landscape-primary");
assert_eq!(value["screenTouch"]["touchType"], "function");
assert_eq!(value["screenTouch"]["touchEventType"], "function");
- assert_eq!(value["screenTouch"]["ontouchstartType"], "object");
+ assert_eq!(value["screenTouch"]["ontouchstartType"], "undefined");
assert_eq!(value["canvasWebgl"]["canvasCtor"], "HTMLCanvasElement");
assert_eq!(value["canvasWebgl"]["canvasTag"], "CANVAS");
@@ -1556,13 +1556,14 @@ fn automation_override_preserves_the_native_webdriver_baseline() {
})
.unwrap();
for enabled in [false, true, false] {
- vm.set_navigator_overrides(&moli_page_types::NavigatorOverrides {
+ vm.set_navigator_overrides_and_sync_surface(&moli_page_types::NavigatorOverrides {
queries: moli_page_types::NavigatorQueryOverrides {
automation: enabled,
..Default::default()
},
..Default::default()
- });
+ })
+ .unwrap();
assert_eq!(
vm.eval("navigator.webdriver").unwrap(),
(baseline || enabled).to_string()
diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/legacy_element_surfaces.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/legacy_element_surfaces.rs
index c3deb6067f..a20553d216 100644
--- a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/legacy_element_surfaces.rs
+++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/legacy_element_surfaces.rs
@@ -730,13 +730,21 @@ fn global_touch_event_handlers_live_on_mixin_owners() {
"https://global-touch-event-handlers.test/",
"",
);
+ vm.set_navigator_overrides_and_sync_surface(&moli_page_types::NavigatorOverrides {
+ max_touch_points: Some(5),
+ ..Default::default()
+ })
+ .unwrap();
let result = vm
.eval(
r#"
(() => {
+ const frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+ return frame.contentWindow.eval('(' + function() {
const names = ['ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel'];
- const owners = [window, HTMLElement.prototype, SVGElement.prototype, Document.prototype];
+ const owners = [window, HTMLElement.prototype, SVGElement.prototype, MathMLElement.prototype, Document.prototype];
const descriptorIsEventHandler = (owner, name) => {
const descriptor = Object.getOwnPropertyDescriptor(owner, name);
return !!descriptor &&
@@ -763,6 +771,7 @@ fn global_touch_event_handlers_live_on_mixin_owners() {
}
return JSON.stringify({ ownAccessors, absentFromElement, calls });
+ }.toString() + ')()');
})()
"#,
)
@@ -774,6 +783,20 @@ fn global_touch_event_handlers_live_on_mixin_owners() {
);
}
+#[test]
+fn desktop_touch_feature_detection_does_not_hide_event_constructors() {
+ let mut vm = new_parsed_test_vm(
+ "https://desktop-touch.test/",
+ "",
+ );
+ assert_eq!(vm.eval(r#"(() => {
+ const names = ['ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel'];
+ const owners = [window, Document.prototype, HTMLElement.prototype, SVGElement.prototype, MathMLElement.prototype];
+ return names.every(name => owners.every(owner => !(name in owner))) &&
+ new TouchEvent('touchstart') instanceof TouchEvent && typeof Touch === 'function';
+ })()"#).unwrap(), "true");
+}
+
#[test]
fn media_accessors_live_on_owner_prototypes() {
let mut vm = new_parsed_test_vm(