fix(dom): scope touch feature detection to native Window lifetime

This commit is contained in:
ldm0
2026-09-29 16:34:35 +08:00
parent 7bf70b5cd7
commit 5f2e207df3
24 changed files with 571 additions and 58 deletions
@@ -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());
@@ -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(),
"<!doctype html><body><script>globalThis.initialTouchHandler = 'ontouchstart' in window;</script>".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 = '<!doctype html><body>child</body>';
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(),
"<!doctype html><body><iframe id='touch-child'></iframe></body>".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(),
"<!doctype html><body>touch</body>".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"]));
}
@@ -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;
@@ -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::<DocumentCreateEventArgs>(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()),
@@ -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<v8::Local<'s, v8::Object>> {
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,
@@ -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",
@@ -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,
@@ -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))
}
@@ -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>,
@@ -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"
@@ -482,6 +482,9 @@ pub(crate) struct LocalWindowRecord {
pub(crate) realm_id: Option<FrameRealmId>,
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<bool>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -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<bool> {
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,
@@ -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();
@@ -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<bool> {
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, '_>,
+16 -1
View File
@@ -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<v8::Local<'s, v8::Context>> {
// 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::<v8::Object>::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>,
@@ -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,
@@ -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,
@@ -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(
+1 -3
View File
@@ -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);
+9 -3
View File
@@ -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<PageTask>,
resource_completion_tx: RendererResourceCompletionSender,
@@ -2229,7 +2231,7 @@ impl ScriptVmDefaultWorldBootstrap {
crate::service_worker_runtime::ServiceWorkerClientId,
>,
) -> std::result::Result<Self, ScriptVmBootstrapError> {
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(
@@ -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,
+16 -1
View File
@@ -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),
@@ -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()
@@ -730,13 +730,21 @@ fn global_touch_event_handlers_live_on_mixin_owners() {
"https://global-touch-event-handlers.test/",
"<!doctype html><html><head></head><body></body></html>",
);
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/",
"<!doctype html><body></body>",
);
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(