mirror of
https://github.com/lexmount/moli.git
synced 2026-10-01 00:00:48 +00:00
fix(dom): scope touch feature detection to native Window lifetime
This commit is contained in:
@@ -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());
|
||||
|
||||
+234
@@ -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, '_>,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
+24
-1
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user