mirror of
https://github.com/lexmount/moli.git
synced 2026-10-31 16:06:30 +00:00
fix(layout): adapt WPT input to explicit published snapshots
This commit is contained in:
@@ -117,6 +117,30 @@ impl<N> FrozenLayoutTree<N>
|
||||
where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
{
|
||||
/// Retains a published viewport for an embedded Document without a root
|
||||
/// element. The document identity is its source, and no box can receive
|
||||
/// input or provide element geometry until a later layout is published.
|
||||
pub fn empty_viewport(source_root: N, viewport: LayoutViewport) -> Self {
|
||||
Self::new(
|
||||
source_root,
|
||||
viewport,
|
||||
LayoutPoint::ZERO,
|
||||
LayoutSize::new(viewport.css_width as f32, viewport.css_height as f32),
|
||||
LayoutOutputBoxId::from_index(0),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
FrozenCoordinateSpace {
|
||||
owner: None,
|
||||
local_to_viewport: super::model::LayoutTransform2D::IDENTITY,
|
||||
local_to_viewport_ignoring_css_transforms:
|
||||
super::model::LayoutTransform2D::IDENTITY,
|
||||
},
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Source root whose complete layout projection this member tree owns.
|
||||
pub const fn source_root(&self) -> N {
|
||||
self.source_root
|
||||
|
||||
@@ -14,6 +14,7 @@ async fn activate(
|
||||
))
|
||||
.await;
|
||||
if matches!(mode, "trusted" | "location" | "nested-click") {
|
||||
page.command("Page.captureScreenshot", json!({})).await;
|
||||
for (kind, buttons) in [("mousePressed", 1), ("mouseReleased", 0)] {
|
||||
page.command("Input.dispatchMouseEvent", json!({
|
||||
"type": kind, "x": 20, "y": 20, "button": "left", "buttons": buttons, "clickCount": 1,
|
||||
|
||||
@@ -162,6 +162,7 @@ async fn navigation_focus_reset_respects_document_policy_and_manual_focus() {
|
||||
.await;
|
||||
// Establish the parent's focus with real input even though its
|
||||
// policy also denies script-initiated focus without activation.
|
||||
page.command("Page.captureScreenshot", json!({})).await;
|
||||
for (kind, buttons) in [("mousePressed", 1), ("mouseReleased", 0)] {
|
||||
page.command("Input.dispatchMouseEvent", json!({
|
||||
"type": kind, "x": 310, "y": 10, "button": "left", "buttons": buttons, "clickCount": 1,
|
||||
@@ -201,6 +202,7 @@ async fn navigation_focus_reset_preserves_trusted_initiation_after_activation_ex
|
||||
"{PROBE}\nsetupNavigationFocusProbe(true, \"'none'\")"
|
||||
))
|
||||
.await;
|
||||
page.command("Page.captureScreenshot", json!({})).await;
|
||||
for (kind, buttons) in [("mousePressed", 1), ("mouseReleased", 0)] {
|
||||
page.command("Input.dispatchMouseEvent", json!({
|
||||
"type": kind, "x": 20, "y": 20, "button": "left", "buttons": buttons, "clickCount": 1,
|
||||
|
||||
@@ -2,8 +2,7 @@ use super::ordering::point_order_handles;
|
||||
use super::*;
|
||||
use crate::native_bridge::document::XHTML_NS;
|
||||
use crate::native_bridge::element::{
|
||||
StyleMode, observable_sources_with_fragments,
|
||||
style_property_value,
|
||||
StyleMode, observable_sources_with_fragments, style_property_value,
|
||||
};
|
||||
use crate::util::string_from_utf16_units_lossy;
|
||||
use std::{cmp::Ordering, collections::HashSet};
|
||||
|
||||
@@ -108,7 +108,13 @@ impl EmbeddedFrameRenderer<DomHandle> for NativeEmbeddedFrameRenderer<'_> {
|
||||
.dom()
|
||||
.document_element_handle_for_document(document)
|
||||
else {
|
||||
return Ok(None);
|
||||
return Ok(Some(moli_layout::EmbeddedFrameSnapshot::new(
|
||||
moli_layout::FrozenLayoutTree::empty_viewport(document, viewport),
|
||||
self.capture_paint.then(|| {
|
||||
moli_layout::PaintSnapshot::new(viewport, moli_layout::PaintColor::TRANSPARENT)
|
||||
}),
|
||||
Vec::new(),
|
||||
)));
|
||||
};
|
||||
let mut services = self
|
||||
.embedded_document_services
|
||||
|
||||
@@ -477,12 +477,13 @@ impl JsContextHost {
|
||||
let root = self
|
||||
.dom_host()
|
||||
.dom()
|
||||
.document_element_handle_for_document(document);
|
||||
.document_element_handle_for_document(document)
|
||||
.unwrap_or(document);
|
||||
let state = self.document_layout_state.borrow();
|
||||
state
|
||||
.latest_layout(document)
|
||||
.or_else(|| root.and_then(|root| state.latest_layout_for_root(root)))
|
||||
.filter(|tree| Some(tree.source_root()) == root)
|
||||
.or_else(|| state.latest_layout_for_root(root))
|
||||
.filter(|tree| tree.source_root() == root)
|
||||
.map(inspect)
|
||||
}
|
||||
|
||||
|
||||
@@ -558,7 +558,6 @@ fn element_at_point(
|
||||
document,
|
||||
moli_layout::LayoutPoint::new(x as f32, y as f32),
|
||||
false,
|
||||
moli_layout::LayoutFlushReason::HitTest,
|
||||
)?;
|
||||
if let Some(element) = hits.into_iter().find_map(|hit| {
|
||||
let element = element_for_hit_source(runtime, hit.source)?;
|
||||
|
||||
@@ -24,13 +24,11 @@ use super::super::forms::{
|
||||
use super::super::{
|
||||
NodePublicEventDispatchOutcome, TextEditInputType, cache_input_files_from_selected_files,
|
||||
closed_details_ancestors_to_reveal, construct_click_event_with_detail_and_modifiers,
|
||||
construct_command_event,
|
||||
construct_input_event, construct_simple_event, contenteditable_editing_host,
|
||||
dispatch_beforeinput, dispatch_popover_hide_events, dispatch_popover_show_events,
|
||||
dispatch_popover_toggle_events,
|
||||
dispatch_public_event, element_attribute, element_has_attribute, form_associated_form_owner,
|
||||
is_disabled_form_control, is_focusable, is_valid_submit_button,
|
||||
label_activation_control_handle,
|
||||
construct_command_event, construct_input_event, construct_simple_event,
|
||||
contenteditable_editing_host, dispatch_beforeinput, dispatch_popover_hide_events,
|
||||
dispatch_popover_show_events, dispatch_popover_toggle_events, dispatch_public_event,
|
||||
element_attribute, element_has_attribute, form_associated_form_owner, is_disabled_form_control,
|
||||
is_focusable, is_valid_submit_button, label_activation_control_handle,
|
||||
perform_popover_invoker_default_action, perform_summary_click_default_action,
|
||||
replace_text_control_selection, resolve_url_like_attribute,
|
||||
resolved_reflected_element_attribute_handle, scroll_node_into_view_at_start,
|
||||
|
||||
@@ -13,7 +13,7 @@ use super::super::{
|
||||
JsContextHost, document::set_detached_text_replacement_value,
|
||||
node::node_runtime_and_handle_from_object_or_detached, throw_dom_exception,
|
||||
};
|
||||
use super::geometry::observable_client_rects;
|
||||
use super::geometry::read_client_rects;
|
||||
use super::{
|
||||
html_element_getter_receiver, html_element_setter_receiver, observable_sources_with_fragments,
|
||||
property_string_value,
|
||||
@@ -832,12 +832,7 @@ fn node_inner_text(
|
||||
&& rendered_state
|
||||
.target_style()
|
||||
.is_some_and(|style| style.display != ComputedDisplayKind::Contents)
|
||||
&& observable_client_rects(
|
||||
runtime,
|
||||
handle,
|
||||
moli_layout::LayoutFlushReason::SynchronousGeometry,
|
||||
)?
|
||||
.is_empty()
|
||||
&& read_client_rects(runtime, handle)?.is_empty()
|
||||
{
|
||||
return Ok(node.text_content(runtime.dom_host().dom()));
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ pub(crate) use events::{
|
||||
dispatch_text_control_event, queue_text_control_document_selection_change_event,
|
||||
};
|
||||
pub(crate) use selection::{
|
||||
replace_text_control_selection,
|
||||
text_control_set_selection_range_internal,
|
||||
replace_text_control_selection, text_control_set_selection_range_internal,
|
||||
text_control_set_selection_range_with_direction_internal,
|
||||
};
|
||||
pub(in crate::native_bridge) use selection::{
|
||||
|
||||
@@ -11,8 +11,8 @@ pub use client_rect::ClientRect;
|
||||
#[cfg(test)]
|
||||
pub(crate) use hit_test::observable_scrollbar_hit_test;
|
||||
pub(crate) use hit_test::{
|
||||
element_is_inert_for_hit_testing, InputSurfaceHit, input_surface_hit_test, observable_deep_hit_test, observable_hit_test,
|
||||
observable_input_hit_test,
|
||||
InputSurfaceHit, element_is_inert_for_hit_testing, input_surface_hit_test,
|
||||
observable_deep_hit_test, observable_hit_test, observable_input_hit_test,
|
||||
};
|
||||
pub(in crate::native_bridge) use metrics::{
|
||||
node_client_height_getter_function, node_client_left_getter_function,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use moli_layout::{
|
||||
FrozenLayoutTree, LayoutControlSurfaceHit, LayoutError, LayoutHit, LayoutPaintedSurfaceHit,
|
||||
LayoutPoint, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutTransform2D, LayoutViewport,
|
||||
LayoutPoint, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutTransform2D,
|
||||
LayoutViewport,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -114,11 +115,9 @@ pub(crate) fn observable_scrollbar_hit_test(
|
||||
document: DomHandle,
|
||||
point: LayoutPoint,
|
||||
) -> Result<Option<LayoutScrollbarHit<DomHandle>>, LayoutError> {
|
||||
input_surface_hit_test(runtime, document, point, false, true).map(|hit| {
|
||||
match hit.control {
|
||||
Some(LayoutControlSurfaceHit::Scrollbar(scrollbar)) => Some(scrollbar),
|
||||
Some(LayoutControlSurfaceHit::ScrollbarCorner(_)) | None => None,
|
||||
}
|
||||
input_surface_hit_test(runtime, document, point, false, true).map(|hit| match hit.control {
|
||||
Some(LayoutControlSurfaceHit::Scrollbar(scrollbar)) => Some(scrollbar),
|
||||
Some(LayoutControlSurfaceHit::ScrollbarCorner(_)) | None => None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -165,13 +164,7 @@ pub(crate) fn observable_deep_hit_test(
|
||||
point: LayoutPoint,
|
||||
ignore_pointer_events_none: bool,
|
||||
) -> Result<Option<DomHandle>, LayoutError> {
|
||||
let hit = input_surface_hit_test(
|
||||
runtime,
|
||||
document,
|
||||
point,
|
||||
ignore_pointer_events_none,
|
||||
false,
|
||||
)?;
|
||||
let hit = input_surface_hit_test(runtime, document, point, ignore_pointer_events_none, false)?;
|
||||
Ok(hit.document_or_element().map(|input| input.handle))
|
||||
}
|
||||
|
||||
@@ -322,7 +315,8 @@ fn input_surface_hit_test_in_tree(
|
||||
.dom_host()
|
||||
.dom()
|
||||
.document_element_handle_for_document(child_document)
|
||||
!= Some(child_tree.source_root())
|
||||
.unwrap_or(child_document)
|
||||
!= child_tree.source_root()
|
||||
{
|
||||
return Err(LayoutError::NoLayoutSnapshot);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ use crate::native_bridge::element::{
|
||||
construct_pointer_event_with_related_target_and_modifiers, construct_simple_event,
|
||||
construct_touch_event, construct_touch_event_with_points, construct_wheel_event,
|
||||
contenteditable_editing_host, dispatch_public_event, input_surface_hit_test, is_text_control,
|
||||
observable_input_hit_test, perform_auxiliary_link_default_action, perform_drop_default_action,
|
||||
observable_input_hit_test, perform_auxiliary_link_default_action,
|
||||
perform_clipboard_key_default_action, perform_drop_default_action,
|
||||
perform_implicit_submission_from_control, perform_mouse_focus_default_action,
|
||||
perform_clipboard_key_default_action,
|
||||
perform_scrollbar_scroll_default_action, perform_wheel_scroll_default_action,
|
||||
replace_contenteditable_selection, replace_text_control_selection,
|
||||
select_contenteditable_contents, text_control_set_selection_range_internal,
|
||||
|
||||
@@ -18,6 +18,7 @@ navigator.clipboard.writeText('seed');'ready'
|
||||
}
|
||||
|
||||
fn activated_copy_eval(vm: &mut StandaloneScriptVmHarness, script: &str) -> String {
|
||||
vm.publish_layout_for_test().unwrap();
|
||||
vm._context_host
|
||||
.borrow_mut()
|
||||
.begin_protocol_user_gesture_activation();
|
||||
|
||||
@@ -338,6 +338,7 @@ fn close_watcher_mouse_and_touch_activate_at_their_respective_input_phases() {
|
||||
setup_close_watcher_input_document(&mut vm);
|
||||
vm.eval("record('first').oncancel = e => e.preventDefault()")
|
||||
.unwrap();
|
||||
vm.publish_layout_for_test().unwrap();
|
||||
if touch {
|
||||
vm.dispatch_touch_event_at_point(10.0, 11.0, "touchstart", false)
|
||||
.unwrap();
|
||||
|
||||
+1
@@ -1425,6 +1425,7 @@ fn single_line_text_input_preserves_programmatic_scroll_across_select() {
|
||||
)
|
||||
.expect("the previous text input should be replaced");
|
||||
|
||||
refresh_layout_for_test(&mut vm);
|
||||
vm.eval("__scrollInput.scrollLeft = 33")
|
||||
.expect("text input should accept a programmatic scroll");
|
||||
refresh_layout_for_test(&mut vm);
|
||||
|
||||
@@ -15,7 +15,6 @@ fn viewport_input_vm() -> StandaloneScriptVmHarness {
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
vm.force_fresh_layout_reads_for_test();
|
||||
vm.eval(
|
||||
r#"
|
||||
if (!document.documentElement) document.appendChild(document.createElement('html'));
|
||||
@@ -37,6 +36,7 @@ fn viewport_input_vm() -> StandaloneScriptVmHarness {
|
||||
}
|
||||
|
||||
fn click_viewport(vm: &mut StandaloneScriptVmHarness, x: f64, y: f64) {
|
||||
vm.publish_layout_for_test().unwrap();
|
||||
for (event, button, buttons) in [("mousemove", -1, 0), ("mousedown", 0, 1), ("mouseup", 0, 0)] {
|
||||
vm.dispatch_mouse_event_at_point(x, y, event, button, Some(buttons), 0.0, 0.0)
|
||||
.unwrap();
|
||||
@@ -240,6 +240,7 @@ fn viewport_root_input_wheel_keeps_child_target_and_chains_at_boundaries() {
|
||||
)
|
||||
.unwrap();
|
||||
vm.eval(setup).unwrap();
|
||||
vm.publish_layout_for_test().unwrap();
|
||||
vm.dispatch_mouse_event_at_point(80.0, 80.0, "wheel", -1, Some(0), 0.0, 120.0)
|
||||
.unwrap();
|
||||
let actual = vm
|
||||
@@ -256,3 +257,46 @@ fn viewport_root_input_wheel_keeps_child_target_and_chains_at_boundaries() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_root_input_requires_republication_when_an_empty_child_gains_a_root() {
|
||||
let mut vm = viewport_input_vm();
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.frame = document.createElement('iframe');
|
||||
frame.style.cssText = 'position:fixed;left:40px;top:40px;width:200px;height:50px;border:4px solid black';
|
||||
document.body.appendChild(frame);
|
||||
frame.contentDocument.documentElement.remove();
|
||||
recordViewportClicks(frame.contentWindow, 'child');
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
click_viewport(&mut vm, 80.0, 80.0);
|
||||
assert_eq!(vm.eval("viewportClicks.length").unwrap(), "0");
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
const child = frame.contentDocument;
|
||||
const root = child.createElement('html');
|
||||
root.style.cssText = 'margin:0;padding:0;height:0';
|
||||
child.appendChild(root);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let before = vm.layout_pass_observability_for_test().1;
|
||||
let error = vm
|
||||
.dispatch_mouse_event_at_point(80.0, 80.0, "mousedown", 0, Some(1), 0.0, 0.0)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error.downcast_ref::<moli_layout::LayoutError>(),
|
||||
Some(&moli_layout::LayoutError::NoLayoutSnapshot)
|
||||
);
|
||||
assert_eq!(vm.layout_pass_observability_for_test().1, before);
|
||||
assert_eq!(vm.pressed_mouse_buttons, 0);
|
||||
assert!(vm.pending_mouse_press.is_none());
|
||||
click_viewport(&mut vm, 80.0, 80.0);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(viewportClicks)").unwrap(),
|
||||
r#"[["child","HTML",36,36,true]]"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -618,6 +618,7 @@ html, body { margin: 0; }
|
||||
<div id="under"></div><div id="container"><div id="target"></div></div>"#,
|
||||
);
|
||||
|
||||
vm.publish_layout_for_test().unwrap();
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
@@ -635,13 +636,32 @@ html, body { margin: 0; }
|
||||
container.inert = false;
|
||||
values.push(top() === target);
|
||||
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
return values.join('|');
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("dynamic inert point-query probe should evaluate");
|
||||
assert_eq!(result, "true|under|under,html|true");
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('style', 'position:fixed;inset:0;width:100px;height:100px');
|
||||
const rect = document.createElementNS(svg.namespaceURI, 'rect');
|
||||
rect.setAttribute('width', '100');
|
||||
rect.setAttribute('height', '100');
|
||||
svg.appendChild(rect);
|
||||
document.body.appendChild(svg);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
vm.publish_layout_for_test().unwrap();
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"(() => {
|
||||
const top = () => document.elementFromPoint(1, 1);
|
||||
const target = document.getElementById('target');
|
||||
const values = [];
|
||||
const svgHit = top();
|
||||
svg.setAttribute('inert', '');
|
||||
values.push(top() === svgHit);
|
||||
@@ -657,5 +677,5 @@ html, body { margin: 0; }
|
||||
)
|
||||
.expect("dynamic inert point-query probe should evaluate");
|
||||
|
||||
assert_eq!(result, "true|under|under,html|true|true|true");
|
||||
assert_eq!(result, "true|true");
|
||||
}
|
||||
|
||||
@@ -1955,6 +1955,8 @@ fn dom_rect_list_has_readonly_indexed_semantics_and_rejects_structured_clone() {
|
||||
"<!doctype html><body><div id='node'>text</div></body>",
|
||||
);
|
||||
|
||||
vm.publish_layout_for_test().unwrap();
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
|
||||
@@ -19,7 +19,7 @@ fn native_element_click_initializes_layout_once() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_element_click_refreshes_stale_layout_after_dom_and_style_changes() {
|
||||
fn native_element_click_reads_explicitly_republished_layout_after_dom_and_style_changes() {
|
||||
for mutation in [
|
||||
"target.style.left='300px'",
|
||||
"const veil=document.createElement('div');veil.style.cssText='position:fixed;inset:0;z-index:999';document.body.appendChild(veil)",
|
||||
@@ -55,11 +55,18 @@ fn native_element_click_refreshes_stale_layout_after_dom_and_style_changes() {
|
||||
"(()=>{{const target=document.getElementById('target');{mutation}}})()"
|
||||
))
|
||||
.expect("mutate live DOM/style before preparing another click");
|
||||
let _ = vm.prepare_element_click(target);
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test().1,
|
||||
before + 1,
|
||||
"click preparation must not publish changed DOM or style"
|
||||
);
|
||||
publish_layout_for_test(&mut vm);
|
||||
let prepared = vm.prepare_element_click(target);
|
||||
let prepared_passes = vm.layout_pass_observability_for_test().1;
|
||||
assert!(
|
||||
prepared_passes > before + 1,
|
||||
"a changed DOM or style must refresh the shared geometry snapshot: {mutation}"
|
||||
"explicit publication must refresh the shared geometry snapshot: {mutation}"
|
||||
);
|
||||
if mutation != "target.style.left='300px'" {
|
||||
assert!(
|
||||
@@ -107,8 +114,8 @@ fn native_element_click_refreshes_stale_layout_after_dom_and_style_changes() {
|
||||
}
|
||||
let passes = vm.layout_pass_observability_for_test().1 - before;
|
||||
assert_eq!(
|
||||
passes, 1,
|
||||
"the entire mouse sequence must reuse one snapshot: {mutation}"
|
||||
passes, 2,
|
||||
"the entire mouse sequence must reuse the republished snapshot: {mutation}"
|
||||
);
|
||||
let events = vm.eval("JSON.stringify([clicks,events])").unwrap();
|
||||
if let Some((existing_passes, existing_events)) = &reference {
|
||||
|
||||
Reference in New Issue
Block a user