diff --git a/moli-layout/src/layout_tree/query.rs b/moli-layout/src/layout_tree/query.rs index 187e473023..533b913c49 100644 --- a/moli-layout/src/layout_tree/query.rs +++ b/moli-layout/src/layout_tree/query.rs @@ -33,7 +33,7 @@ pub struct LayoutDocumentMetrics { pub content_size: LayoutSize, } -/// CSSOM View and observer metrics for one source element. +/// CSSOM View metrics for one source element. /// /// Transformed quads use viewport CSS pixels and therefore include CSS /// `zoom`. Offset, size, and scroll fields are untransformed and have the @@ -65,6 +65,25 @@ pub struct LayoutElementMetrics { pub pointer_events: bool, } +/// One ResizeObserver box size in logical CSS pixels. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct LayoutResizeObserverSize { + pub inline_size: f32, + pub block_size: f32, +} + +/// CSS-box ResizeObserver sizes projected wholly from one frozen principal box. +/// The physical content size is used for contentRect width/height. The logical +/// content and border pairs use the writing axes and effective zoom captured +/// with the geometry, independently of authored box-sizing. This owned answer +/// does not add state to the frozen tree or provide SVG bounding-box semantics. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct LayoutResizeObserverMetrics { + pub content_size: LayoutSize, + pub content_box_size: LayoutResizeObserverSize, + pub border_box_size: LayoutResizeObserverSize, +} + /// One scroll container on a target's layout ancestor chain. #[derive(Clone, Debug, PartialEq)] pub struct LayoutScrollContainerMetrics { @@ -113,6 +132,10 @@ pub enum LayoutQuery { ElementMetrics { source: N, }, + /// Physical content dimensions and logical CSS content/border sizes for RO. + ResizeObserverMetrics { + source: N, + }, /// Used Grid row and column tracks for a principal Grid container. UsedGridTracks { source: N, @@ -166,6 +189,7 @@ pub enum LayoutQueryAnswer { ContentQuads(Vec), TextRangeRects(Vec), ElementMetrics(Option>), + ResizeObserverMetrics(Option), UsedGridTracks(Option), ScrollIntoViewGeometry(Option>), IntersectionGeometry(Option), @@ -309,6 +333,11 @@ where LayoutQuery::ElementMetrics { source } => { LayoutQueryAnswer::ElementMetrics(self.element_metrics_for_source(*source)) } + LayoutQuery::ResizeObserverMetrics { source } => { + LayoutQueryAnswer::ResizeObserverMetrics( + self.resize_observer_metrics_for_source(*source), + ) + } LayoutQuery::UsedGridTracks { source } => { LayoutQueryAnswer::UsedGridTracks(self.used_grid_tracks_for_source(*source)) } diff --git a/moli-layout/src/layout_tree/source_query.rs b/moli-layout/src/layout_tree/source_query.rs index efd44fca9a..1355a12a23 100644 --- a/moli-layout/src/layout_tree/source_query.rs +++ b/moli-layout/src/layout_tree/source_query.rs @@ -10,7 +10,10 @@ use super::{ LayoutOutputBoxId, LayoutPoint, LayoutQuad, LayoutRect, LayoutResolvedGridTracks, LayoutSize, LayoutTransform2D, LayoutUsedSize, }, - query::{LayoutElementMetrics, LayoutNodeOutput}, + query::{ + LayoutElementMetrics, LayoutNodeOutput, LayoutResizeObserverMetrics, + LayoutResizeObserverSize, + }, tree::{CssSizingBox, FrozenLayoutBox, FrozenLayoutTree}, }; @@ -104,6 +107,19 @@ where self.element_metrics_for_source_with_offset_parent_filter(source, |_| true) } + /// ResizeObserver's CSS-box sizes, interpreted using only metadata and + /// geometry from this snapshot. Non-replaced inline and suppressed sources + /// have no applicable CSS sizing box and produce no measurement here. + pub fn resize_observer_metrics_for_source( + &self, + source: N, + ) -> Option { + self.boxes + .iter() + .find(|layout_box| layout_box.principal_source == Some(source))? + .resize_observer_metrics() + } + /// Physical and logical CSSOM sizes from this snapshot's sizing box, /// without transforms, integer rounding, or absolute CSS zoom. A source /// without an applicable principal box keeps its computed value. @@ -549,6 +565,35 @@ where } impl FrozenLayoutBox { + fn resize_observer_metrics(&self) -> Option { + let sizing = self.css_sizing?; + let unzoom = CssomAbsoluteZoom::new(self.effective_zoom); + let content_size = unzoom.size(LayoutSize::new( + self.content_box.width, + self.content_box.height, + )); + let border_size = unzoom.size(LayoutSize::new( + self.border_box.width, + self.border_box.height, + )); + let logical = |size: LayoutSize| { + let (inline_size, block_size) = if sizing.horizontal { + (size.width, size.height) + } else { + (size.height, size.width) + }; + LayoutResizeObserverSize { + inline_size, + block_size, + } + }; + Some(LayoutResizeObserverMetrics { + content_size, + content_box_size: logical(content_size), + border_box_size: logical(border_size), + }) + } + fn used_size(&self) -> Option { let sizing = self.css_sizing?; let rect = match sizing.box_kind { diff --git a/moli-layout/src/lib.rs b/moli-layout/src/lib.rs index 35df166261..3375ce70ad 100644 --- a/moli-layout/src/lib.rs +++ b/moli-layout/src/lib.rs @@ -59,10 +59,11 @@ pub use layout_tree::{ LayoutFragmentBoxModel, LayoutFragmentId, LayoutFragmentKind, LayoutHit, LayoutIntersectionGeometry, LayoutNodeOutput, LayoutOutputBoxId, LayoutPaintedSurfaceHit, LayoutPassMetrics, LayoutPassResult, LayoutPoint, LayoutQuad, LayoutQuery, LayoutQueryAnswer, - LayoutQueryBatch, LayoutRect, LayoutResolvedGridTrackList, LayoutResolvedGridTracks, - LayoutScrollContainerMetrics, LayoutScrollExtent, LayoutScrollIntoViewGeometry, LayoutSize, - LayoutTransform2D, LayoutTreeRetentionMetrics, LayoutUsedSize, LayoutViewport, - MAX_RETAINED_LAYOUT_BOXES, MAX_RETAINED_LAYOUT_FRAGMENTS, MAX_RETAINED_LAYOUT_TREE_BYTES, + LayoutQueryBatch, LayoutRect, LayoutResizeObserverMetrics, LayoutResizeObserverSize, + LayoutResolvedGridTrackList, LayoutResolvedGridTracks, LayoutScrollContainerMetrics, + LayoutScrollExtent, LayoutScrollIntoViewGeometry, LayoutSize, LayoutTransform2D, + LayoutTreeRetentionMetrics, LayoutUsedSize, LayoutViewport, MAX_RETAINED_LAYOUT_BOXES, + MAX_RETAINED_LAYOUT_FRAGMENTS, MAX_RETAINED_LAYOUT_TREE_BYTES, }; pub use normalize::{NormalizedBoxNode, NormalizedBoxTree, NormalizedFormattingContext}; pub use normalize_source::{ diff --git a/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs b/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs index 0cb1db9a8a..6d73fa8bb5 100644 --- a/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs @@ -454,7 +454,7 @@ fn for_each_resize_observation<'s, 'i>( handles .iter() .copied() - .map(|source| moli_layout::LayoutQuery::ElementMetrics { source }), + .map(|source| moli_layout::LayoutQuery::ResizeObserverMetrics { source }), ); let answers = match crate::native_bridge::element::published_geometry_batch( runtime, @@ -480,10 +480,10 @@ fn for_each_resize_observation<'s, 'i>( } }; for (handle, answer) in handles.into_iter().zip(answers) { - let moli_layout::LayoutQueryAnswer::ElementMetrics(metrics) = answer else { + let moli_layout::LayoutQueryAnswer::ResizeObserverMetrics(metrics) = answer else { return Err(moli_layout::LayoutError::source_contract( "ResizeObserver geometry", - "provider returned a mismatched element-metrics answer", + "provider returned a mismatched ResizeObserver-metrics answer", )); }; geometry.insert(handle, (metrics, dpr)); @@ -493,43 +493,36 @@ fn for_each_resize_observation<'s, 'i>( for entry in pending { let (metrics, dpr) = entry .handle - .and_then(|handle| geometry.get(&handle).cloned()) + .and_then(|handle| geometry.get(&handle).copied()) .and_then(|(metrics, dpr)| metrics.map(|metrics| (metrics, dpr))) - .map(|(metrics, dpr)| (Some(metrics), dpr)) - .unwrap_or((None, 1.0)); - let content_width = metrics - .as_ref() - .map(|metrics| f64::from(metrics.content_size.width)) - .unwrap_or(0.0); - let content_height = metrics - .as_ref() - .map(|metrics| f64::from(metrics.content_size.height)) - .unwrap_or(0.0); - let border_width = metrics - .as_ref() - .map(|metrics| f64::from(metrics.offset_size.width)) - .unwrap_or(0.0); - let border_height = metrics - .as_ref() - .map(|metrics| f64::from(metrics.offset_size.height)) - .unwrap_or(0.0); - let rect = (0.0, 0.0, content_width, content_height); - let content_box_size = (content_width, content_height); - let border_box_size = (border_width, border_height); + .unwrap_or((moli_layout::LayoutResizeObserverMetrics::default(), 1.0)); + let rect = ( + 0.0, + 0.0, + f64::from(metrics.content_size.width), + f64::from(metrics.content_size.height), + ); + let content_box_size = ( + f64::from(metrics.content_box_size.inline_size), + f64::from(metrics.content_box_size.block_size), + ); + let border_box_size = ( + f64::from(metrics.border_box_size.inline_size), + f64::from(metrics.border_box_size.block_size), + ); + // Preserve the existing device-pixel approximation, now on the same + // logical axes. Effective zoom and positional snapping are separate. let device_pixel_content_box_size = ( - (content_width * f64::from(dpr)).round(), - (content_height * f64::from(dpr)).round(), + (content_box_size.0 * f64::from(dpr)).round(), + (content_box_size.1 * f64::from(dpr)).round(), ); let observed_size = match entry .record .and_then(|record| observed_record_box(scope, record)) { - Some(ResizeObserverBoxOptions::BorderBox) => (border_width, border_height), - Some(ResizeObserverBoxOptions::DevicePixelContentBox) => ( - (content_width * f64::from(dpr)).round(), - (content_height * f64::from(dpr)).round(), - ), - Some(ResizeObserverBoxOptions::ContentBox) | None => (content_width, content_height), + Some(ResizeObserverBoxOptions::BorderBox) => border_box_size, + Some(ResizeObserverBoxOptions::DevicePixelContentBox) => device_pixel_content_box_size, + Some(ResizeObserverBoxOptions::ContentBox) | None => content_box_size, }; if compare_reported_sizes && let Some(record) = entry.record { let previous = resize_observer_last_reported_size(scope, record); diff --git a/moli-renderer-v8/src/native_bridge/element/geometry/mock/queries.rs b/moli-renderer-v8/src/native_bridge/element/geometry/mock/queries.rs index e511dcfa69..ea36a7ca03 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry/mock/queries.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry/mock/queries.rs @@ -4,7 +4,8 @@ use moli_layout::{ LayoutAnswers, LayoutBoxModel, LayoutCaretPosition, LayoutDocumentMetrics, LayoutElementMetrics, LayoutFlushReason, LayoutHit, LayoutIntersectionGeometry, LayoutPassMetrics, LayoutPoint, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, - LayoutScrollContainerMetrics, LayoutScrollIntoViewGeometry, LayoutSize, + LayoutResizeObserverMetrics, LayoutResizeObserverSize, LayoutScrollContainerMetrics, + LayoutScrollIntoViewGeometry, LayoutSize, }; use super::super::client_rect::{quad_from_client_rect, zero_client_rect}; @@ -107,6 +108,22 @@ pub(crate) fn answer_query( LayoutQuery::ElementMetrics { source } => { LayoutQueryAnswer::ElementMetrics(mock_element_metrics(runtime, *source)) } + LayoutQuery::ResizeObserverMetrics { source } => { + // Keep Mock's existing approximate physical-as-logical contract. + LayoutQueryAnswer::ResizeObserverMetrics(mock_element_metrics(runtime, *source).map( + |metrics| LayoutResizeObserverMetrics { + content_size: metrics.content_size, + content_box_size: LayoutResizeObserverSize { + inline_size: metrics.content_size.width, + block_size: metrics.content_size.height, + }, + border_box_size: LayoutResizeObserverSize { + inline_size: metrics.offset_size.width, + block_size: metrics.offset_size.height, + }, + }, + )) + } LayoutQuery::UsedGridTracks { .. } => LayoutQueryAnswer::UsedGridTracks(None), LayoutQuery::ScrollIntoViewGeometry { source } => { LayoutQueryAnswer::ScrollIntoViewGeometry(mock_scroll_into_view_geometry( diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_resize_observer.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_resize_observer.rs index 9c3ea48ec6..85d253da0f 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_resize_observer.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_resize_observer.rs @@ -2,6 +2,7 @@ use super::realtime_opportunity::run_opportunity; use super::*; use moli_page_types::LayoutPolicy; +mod geometry; mod lifecycle; mod ownership; mod rewrites; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_resize_observer/geometry.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_resize_observer/geometry.rs new file mode 100644 index 0000000000..6a3c48cf68 --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_resize_observer/geometry.rs @@ -0,0 +1,184 @@ +use super::*; + +fn observe_axis_boxes(page: &mut PageVm, size: &str) { + page.vm_mut().eval(&format!(r#" + document.body.innerHTML='
'; + window.samples={{content:[],border:[]}}; + window.record=e=>[ + e.contentRect.width,e.contentRect.height, + e.contentBoxSize[0].inlineSize,e.contentBoxSize[0].blockSize, + e.borderBoxSize[0].inlineSize,e.borderBoxSize[0].blockSize + ]; + window.contentObserver=new ResizeObserver(entries=>samples.content.push(...entries.map(record))); + window.borderObserver=new ResizeObserver(entries=>samples.border.push(...entries.map(record))); + contentObserver.observe(target); + borderObserver.observe(target,{{box:'border-box'}}); + "#)).unwrap(); +} + +#[test] +fn realtime_ro_physical_dimensions_notify_when_only_logical_axes_change() { + run_page_vm_large_stack_async_test("realtime-ro-physical-axis-change", || async move { + let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap(); + let mut page = realtime_page(&loader); + observe_axis_boxes(&mut page, "width:50px;height:100px"); + run_opportunity(&mut page, &loader).await.unwrap(); + assert_eq!( + page.vm_mut().eval("JSON.stringify([samples.content.map(x=>x.slice(0,2)),samples.border.map(x=>x.slice(0,2))])").unwrap(), + "[[[50,100]],[[50,100]]]" + ); + + page.vm_mut() + .eval("target.style.writingMode='horizontal-tb'") + .unwrap(); + run_opportunity(&mut page, &loader).await.unwrap(); + assert_eq!( + page.vm_mut() + .eval("JSON.stringify([samples.content.length,samples.border.length])") + .unwrap(), + "[2,2]", + "changing only logical axes is a size change for both observed boxes" + ); + let expected = r#"{"content":[[50,100,100,50,104,54],[50,100,50,100,54,104]],"border":[[50,100,100,50,104,54],[50,100,50,100,54,104]]}"#; + assert_eq!( + page.vm_mut().eval("JSON.stringify(samples)").unwrap(), + expected + ); + let builds = page.vm().layout_pass_observability_for_test().1; + page.vm_mut().eval("requestAnimationFrame(()=>{})").unwrap(); + run_opportunity(&mut page, &loader).await.unwrap(); + assert_eq!( + page.vm_mut().eval("JSON.stringify(samples)").unwrap(), + expected + ); + assert_eq!(page.vm().layout_pass_observability_for_test().1, builds); + assert_eq!( + page.vm() + .layout_pass_observability_for_test() + .3 + .unwrap() + .paint_operation_count, + 0 + ); + assert!( + page.next_rendering_opportunity_deadline_for_test() + .is_none() + ); + assert!(page.vm().next_timeout_deadline().is_none()); + }); +} + +#[test] +fn realtime_ro_logical_dimensions_do_not_notify_when_only_physical_axes_change() { + run_page_vm_large_stack_async_test("realtime-ro-logical-axis-change", || async move { + let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap(); + let mut page = realtime_page(&loader); + observe_axis_boxes(&mut page, "inline-size:100px;block-size:50px"); + run_opportunity(&mut page, &loader).await.unwrap(); + let expected = r#"{"content":[[50,100,100,50,104,54]],"border":[[50,100,100,50,104,54]]}"#; + assert_eq!( + page.vm_mut().eval("JSON.stringify([samples.content.map(x=>x.slice(0,2)),samples.border.map(x=>x.slice(0,2))])").unwrap(), + "[[[50,100]],[[50,100]]]", + "the ordinary backend must first resolve these logical CSS dimensions" + ); + + page.vm_mut() + .eval("target.style.writingMode='horizontal-tb'") + .unwrap(); + run_opportunity(&mut page, &loader).await.unwrap(); + assert_eq!( + page.vm_mut() + .eval("JSON.stringify([samples.content.length,samples.border.length])") + .unwrap(), + "[1,1]", + "unchanged logical sizes must not produce a second notification" + ); + assert_eq!( + page.vm_mut().eval("JSON.stringify(samples)").unwrap(), + expected + ); + let builds = page.vm().layout_pass_observability_for_test().1; + // The observer decision above must use the frame's already-current + // layout. This getter proves the physical box swapped without fixing it. + assert_eq!( + page.vm_mut() + .eval("JSON.stringify([target.offsetWidth,target.offsetHeight])") + .unwrap(), + "[104,54]" + ); + assert_eq!(page.vm().layout_pass_observability_for_test().1, builds); + page.vm_mut().eval("requestAnimationFrame(()=>{})").unwrap(); + run_opportunity(&mut page, &loader).await.unwrap(); + assert_eq!( + page.vm_mut().eval("JSON.stringify(samples)").unwrap(), + expected + ); + assert_eq!(page.vm().layout_pass_observability_for_test().1, builds); + assert_eq!( + page.vm() + .layout_pass_observability_for_test() + .3 + .unwrap() + .paint_operation_count, + 0 + ); + assert!( + page.next_rendering_opportunity_deadline_for_test() + .is_none() + ); + assert!(page.vm().next_timeout_deadline().is_none()); + }); +} + +#[test] +fn realtime_ro_uses_the_axes_frozen_with_each_published_sample() { + run_page_vm_large_stack_async_test("realtime-ro-published-axes", || async move { + for force in [false, true] { + let loader = + crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap(); + let mut page = realtime_page(&loader); + page.vm_mut().eval(&format!(r#" + document.body.innerHTML='
'; + window.order=[];window.errors=[]; + window.onerror=message=>{{errors.push(message);return true}}; + window.first=new ResizeObserver(()=>{{order.push('first');b.style.writingMode='horizontal-tb';if({force})void b.offsetWidth}}); + window.second=new ResizeObserver(entries=>order.push(entries.map(e=>[ + e.contentRect.width,e.contentRect.height, + e.contentBoxSize[0].inlineSize,e.contentBoxSize[0].blockSize, + e.borderBoxSize[0].inlineSize,e.borderBoxSize[0].blockSize + ]))); + first.observe(a);second.observe(b); + "#)).unwrap(); + run_opportunity(&mut page, &loader).await.unwrap(); + assert_eq!( + page.vm_mut() + .eval("JSON.stringify([order,errors])") + .unwrap(), + if force { + r#"[["first",[[50,100,50,100,50,100]]],[]]"# + } else { + r#"[["first",[[50,100,100,50,100,50]]],["ResizeObserver loop completed with undelivered notifications."]]"# + } + ); + if !force { + run_opportunity(&mut page, &loader).await.unwrap(); + assert_eq!( + page.vm_mut().eval("JSON.stringify(order)").unwrap(), + r#"["first",[[50,100,100,50,100,50]],[[50,100,50,100,50,100]]]"# + ); + } + assert!( + page.next_rendering_opportunity_deadline_for_test() + .is_none() + ); + assert_eq!( + page.vm() + .layout_pass_observability_for_test() + .3 + .unwrap() + .paint_operation_count, + 0 + ); + } + }); +} diff --git a/moli-renderer-v8/src/script_vm/tests/resize_observer_entries.rs b/moli-renderer-v8/src/script_vm/tests/resize_observer_entries.rs index ccc78d6f89..1b3e55d5ad 100644 --- a/moli-renderer-v8/src/script_vm/tests/resize_observer_entries.rs +++ b/moli-renderer-v8/src/script_vm/tests/resize_observer_entries.rs @@ -176,3 +176,87 @@ async fn resize_observer_entries_preserve_webidl_snapshots_and_callback_realms() assert_eq!(result["failures"], serde_json::json!([]), "{name}"); } } + +#[test] +fn ondemand_ro_uses_published_css_box_axes_and_unzoomed_dimensions() { + let mut vm = new_storage_html_test_vm("https://resize-published-axis.test/"); + vm.set_layout_policy(moli_page_types::LayoutPolicy::OnDemand); + vm.eval(r#" + document.body.innerHTML='
'; + window.samples=[]; + "#).unwrap(); + vm.publish_layout().unwrap(); + let builds = vm.layout_pass_observability_for_test().1; + vm.eval( + r#" + window.observer=new ResizeObserver(entries=>samples.push(...entries.map(e=>[ + e.contentRect.width,e.contentRect.height, + e.contentBoxSize[0].inlineSize,e.contentBoxSize[0].blockSize, + e.borderBoxSize[0].inlineSize,e.borderBoxSize[0].blockSize + ]))); + observer.observe(target); + "#, + ) + .unwrap(); + assert_eq!( + vm.eval("JSON.stringify(samples)").unwrap(), + "[[50,100,100,50,104,54]]" + ); + assert_eq!(vm.layout_pass_observability_for_test().1, builds); + + vm.eval("target.style.writingMode='horizontal-tb'").unwrap(); + assert_eq!( + vm.eval("JSON.stringify(samples)").unwrap(), + "[[50,100,100,50,104,54]]", + "OnDemand must continue sampling the axes frozen with its previous layout" + ); + assert_eq!(vm.layout_pass_observability_for_test().1, builds); + vm.publish_layout().unwrap(); + let builds = vm.layout_pass_observability_for_test().1; + // Publishing a snapshot is not itself a legacy RO scheduling operation. + // This mutation schedules its existing check without requesting layout. + vm.eval("target.setAttribute('data-resample','yes')") + .unwrap(); + assert_eq!( + vm.eval("JSON.stringify(samples)").unwrap(), + "[[50,100,100,50,104,54],[50,100,50,100,54,104]]" + ); + assert_eq!(vm.layout_pass_observability_for_test().1, builds); + assert_eq!( + vm.layout_pass_observability_for_test() + .3 + .unwrap() + .paint_operation_count, + 0 + ); +} + +#[test] +fn real_ro_css_box_projection_excludes_non_replaced_inline_fragments() { + let mut vm = new_storage_html_test_vm("https://resize-css-box-kind.test/"); + vm.set_layout_policy(moli_page_types::LayoutPolicy::OnDemand); + vm.eval(r#" + document.body.innerHTML='text'; + window.samples=[]; + "#).unwrap(); + vm.publish_layout().unwrap(); + let builds = vm.layout_pass_observability_for_test().1; + vm.eval( + r#" + window.observer=new ResizeObserver(entries=>samples.push(...entries.map(e=>[ + e.target.id,e.contentRect.width,e.contentRect.height, + e.contentBoxSize[0].inlineSize,e.contentBoxSize[0].blockSize, + e.borderBoxSize[0].inlineSize,e.borderBoxSize[0].blockSize + ]))); + observer.observe(document.getElementById('inline')); + observer.observe(document.getElementById('atomic')); + "#, + ) + .unwrap(); + assert_eq!( + vm.eval("JSON.stringify(samples)").unwrap(), + r#"[["inline",0,0,0,0,0,0],["atomic",50,100,100,50,104,54]]"#, + "a non-replaced inline's CSSOM fragment union is not an RO CSS box" + ); + assert_eq!(vm.layout_pass_observability_for_test().1, builds); +}