diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 6bb77ca8f7..475c37552a 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -81,6 +81,15 @@ clipboard-apis/clipboard-events-synthetic.html clipboard-apis/data-transfer-file-list-change-reference-updates.html clipboard-apis/dataTransfer-clearData.html clipboard-apis/drag-multiple-urls.html +close-watcher/abortsignal.html +close-watcher/inside-event-listeners.html +close-watcher/user-activation/ny-activate-preventDefault.html?CloseWatcher +close-watcher/user-activation/nyyn-CloseWatcher.html +close-watcher/user-activation/nyyyn-CloseWatcher.html +close-watcher/user-activation/yn-activate.html?CloseWatcher +close-watcher/user-activation/yy.html?CloseWatcher +close-watcher/user-activation/yyn.html?CloseWatcher +close-watcher/user-activation/yyy.html?CloseWatcher compat/css-style-declaration-alias-enumeration.html compat/historical.html compat/webkit-box-removing-triggering-anonymous-merge.html diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout.rs b/moli-renderer-v8/src/native_bridge/context_host/layout.rs index 0e68a90b5b..9c17307685 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout.rs @@ -7,7 +7,7 @@ use moli_layout::{ use super::JsContextHost; use super::layout_snapshot::LayoutEnvironment; -use super::layout_state::InferredFrameStyleViewportCacheKey; +use super::layout_state::{InferredFrameStyleViewportCacheKey, LayoutSnapshotInputs}; use crate::{ css_resource_urls::{CompletedStylesheetWebFont, StylesheetLoadBlockingResource}, document_runtime::DomHandle, @@ -327,16 +327,18 @@ impl JsContextHost { (frame, viewport) }) .collect::>(); - let tree = pass.into_tree(); let frame_viewports_changed = { let mut state = self.document_layout_state.borrow_mut(); - state.publish_latest_layout(document, tree, environment); state.update_frame_viewports(frame_viewports) }; if frame_viewports_changed { self.style_viewport_generation .set(self.style_viewport_generation.get().saturating_add(1)); } + let inputs = self.layout_snapshot_inputs(document); + self.document_layout_state + .borrow_mut() + .publish_latest_layout(document, pass.into_tree(), environment, inputs); self.last_layout_pass_metrics.set(Some(metrics)); self.layout_snapshot_cache_publishes .set(self.layout_snapshot_cache_publishes.get().saturating_add(1)); @@ -396,7 +398,7 @@ impl JsContextHost { if self.force_fresh_layout_reads_for_test { return None; } - if !self + if !self.layout_snapshot_inputs_are_current(document) || !self .document_layout_state .borrow() .latest_layout_matches_environment(self.layout_environment()) @@ -409,6 +411,27 @@ impl JsContextHost { .flatten() } + fn layout_snapshot_inputs(&self, document: DomHandle) -> LayoutSnapshotInputs { + let mut documents = self.active_layout_document_handles(); + documents.push(document); + LayoutSnapshotInputs { + dom_version: self.dom_host().dom_version(), + style_generations: self + .style_engine + .computed_style_observation_generations(documents), + style_viewport_generation: self.style_viewport_generation.get(), + environment: StyloStyleEnvironment::from_emulated_media(self.emulated_media()), + visual_resource_generation: self.visual_resource_generation(), + } + } + + fn layout_snapshot_inputs_are_current(&self, document: DomHandle) -> bool { + let inputs = self.layout_snapshot_inputs(document); + self.document_layout_state + .borrow() + .latest_layout_inputs_match(&inputs) + } + /// Inspects the member tree for one exact Document in the single latest /// recursively frozen snapshot. /// diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs index fea61b5630..4cef66d4ed 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs @@ -24,6 +24,18 @@ struct CachedInferredFrameStyleViewport { viewport: StyleViewport, } +/// Inputs sampled by the owner of the latest layout pass. The frozen tree +/// remains available to snapshot consumers, but synchronous geometry queries +/// must rebuild it after any of these inputs change. +#[derive(PartialEq)] +pub(super) struct LayoutSnapshotInputs { + pub(super) dom_version: u64, + pub(super) style_generations: Vec<(DomHandle, u64, u64, u64)>, + pub(super) style_viewport_generation: u64, + pub(super) environment: StyloStyleEnvironment, + pub(super) visual_resource_generation: u64, +} + /// Layout-facing state whose lifetime is bounded by exactly one main Document. /// /// `ScriptVm` outlives `document.open()`, so the main-document owner @@ -43,6 +55,7 @@ pub(super) struct DocumentLayoutState { web_font_resource_generation: Option, visual_state_generation: u64, latest_layout: LatestLayoutTreeCache, + latest_layout_inputs: Option, /// Last used content viewport published by each live iframe owner's /// parent layout. Blink keeps the equivalent size on LocalFrameView; it is /// separate from the single latest-tree slot because a later fresh layout @@ -140,16 +153,23 @@ impl DocumentLayoutState { document: DomHandle, tree: FrozenLayoutTree, environment: LayoutEnvironment, + inputs: LayoutSnapshotInputs, ) { self.latest_layout.publish(document, tree, environment); + self.latest_layout_inputs = Some(inputs); } pub(super) fn latest_layout_matches_environment(&self, environment: LayoutEnvironment) -> bool { self.latest_layout.matches_environment(environment) } + pub(super) fn latest_layout_inputs_match(&self, inputs: &LayoutSnapshotInputs) -> bool { + self.latest_layout_inputs.as_ref() == Some(inputs) + } + pub(super) fn clear_latest_layout(&mut self) { self.latest_layout.clear(); + self.latest_layout_inputs = None; self.mark_visual_state_dirty(); } diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/computed_size/sampling.rs b/moli-renderer-v8/src/runtime/page_vm/tests/computed_size/sampling.rs index 869d4b0131..0d13b367a2 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/computed_size/sampling.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/computed_size/sampling.rs @@ -72,7 +72,7 @@ async fn computed_size_held_getters_observe_grid_sampling_without_dom_mutation() } #[tokio::test(flavor = "current_thread")] -async fn computed_size_grid_sampling_reuses_old_geometry_until_explicit_refresh() { +async fn computed_size_grid_sampling_refreshes_after_style_mutation() { run_page_vm_async_test(async move { let mut page = page_with_size_fixture(GRID)?; page.set_viewport_surface(Some(crate::protocol_types::ViewportSurface { @@ -116,14 +116,13 @@ async fn computed_size_grid_sampling_reuses_old_geometry_until_explicit_refresh( ); assert_eq!( page.vm_mut().eval("held.gridTemplateColumns")?, - "40px 120px" + "100px 100px" ); - assert_eq!(page.vm().layout_pass_observability_for_test().1, passes + 1); + assert_eq!(page.vm().layout_pass_observability_for_test().1, passes + 2); assert_eq!( page.vm().layout_snapshot_cache_observability_for_test().2, - sampled.2 + sampled.2 + 1 ); - publish_size_layout(&mut page)?; assert_eq!( held_sizes(&mut page)?, json!(["200px", "40px", "200px", "40px"]) @@ -133,6 +132,12 @@ async fn computed_size_grid_sampling_reuses_old_geometry_until_explicit_refresh( "100px 100px" ); assert_eq!(page.vm().layout_pass_observability_for_test().1, passes + 2); + publish_size_layout(&mut page)?; + assert_eq!( + held_sizes(&mut page)?, + json!(["200px", "40px", "200px", "40px"]) + ); + assert_eq!(page.vm().layout_pass_observability_for_test().1, passes + 3); Ok::<_, anyhow::Error>(()) }) .await diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/grid_resolved_track_values.rs b/moli-renderer-v8/src/runtime/page_vm/tests/grid_resolved_track_values.rs index 4804f8a3b1..b482d50eba 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/grid_resolved_track_values.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/grid_resolved_track_values.rs @@ -1,7 +1,7 @@ use super::*; #[tokio::test(flavor = "current_thread")] -async fn computed_style_serializes_used_grid_tracks_from_the_frozen_layout_tree() { +async fn computed_style_refreshes_used_grid_tracks_after_style_mutation() { run_page_vm_async_test(async move { let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); @@ -89,6 +89,7 @@ vertical:getComputedStyle(document.getElementById('vertical')).gridTemplateColum "resolved horizontal Grid longhands must expose used tracks while preserving expanded line names, without publishing physical-axis values for vertical Grid", ); + let passes = page_vm.vm().layout_pass_observability_for_test().1; page_vm .vm_mut() .eval("document.getElementById('named').style.cssText='width:400px;grid-template-columns:[new] 1fr 1fr';'mutated'")?; @@ -96,9 +97,17 @@ vertical:getComputedStyle(document.getElementById('vertical')).gridTemplateColum page_vm.vm_mut().eval( "getComputedStyle(document.getElementById('named')).gridTemplateColumns", )?, - "[a] 21px [b c] 22px [d] 23px [e c] 22px [d] 23px [e f] 189px [g]", - "a synchronous style read must stay on the last published layout epoch", + "[new] 200px 200px", + "a synchronous used-track read must reflect the current grid styles", ); + assert_eq!(page_vm.vm().layout_pass_observability_for_test().1, passes + 1); + assert_eq!( + page_vm.vm_mut().eval( + "getComputedStyle(document.getElementById('named')).gridTemplateColumns", + )?, + "[new] 200px 200px", + ); + assert_eq!(page_vm.vm().layout_pass_observability_for_test().1, passes + 1); page_vm .vm_mut() .screenshot_layout_snapshot(moli_layout::PaintViewport::new(400, 300, 1.0))? @@ -110,6 +119,7 @@ vertical:getComputedStyle(document.getElementById('vertical')).gridTemplateColum "[new] 200px 200px", "a screenshot must publish the new Grid track sizes", ); + assert_eq!(page_vm.vm().layout_pass_observability_for_test().1, passes + 2); Ok::<_, anyhow::Error>(()) }) .await diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs index 61178a846b..7d8b9f4db5 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs @@ -116,7 +116,7 @@ getComputedStyle(document.getElementById('fallback')).display } #[tokio::test(flavor = "current_thread")] -async fn geometry_batch_reuses_latest_tree_at_same_viewport_until_fresh_paint_replaces_it() { +async fn geometry_batch_reuses_current_tree_and_refreshes_after_mutation() { run_page_vm_async_test(async move { let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); @@ -226,6 +226,15 @@ document.body.innerHTML = '
< "{transformed_rect:?}" ); + let reused = moli_layout::GeometryProvider::answer( + page_vm.vm_mut(), + moli_layout::LayoutFlushReason::SynchronousGeometry, + moli_layout::LayoutViewport::new(320, 200, 1.0), + &batch, + )?; + assert_eq!(reused.metrics, first.metrics); + assert_eq!(page_vm.vm().layout_pass_observability_for_test(), after_first); + page_vm .vm_mut() .eval("document.getElementById('target').style.width='180px'; 'mutated'")?; @@ -240,20 +249,21 @@ document.body.innerHTML = '
< .vm() .layout_snapshot_cache_observability_for_test(); assert!(!after_second.0); - assert_eq!(after_second.1, before.1 + 1); + assert_eq!(after_second.1, before.1 + 2); assert_eq!(cache_after_second.0, cache_before.0 + 1); - assert_eq!(cache_after_second.1, cache_before.1 + 1); - assert_eq!(cache_after_second.2, cache_before.2 + 1); + assert_eq!(cache_after_second.1, cache_before.1 + 2); + assert_eq!(cache_after_second.2, cache_before.2 + 2); let second_width = match &second.answers[1] { moli_layout::LayoutQueryAnswer::BoxModel(Some(model)) => { model.border.bounding_rect().width } answer => panic!("unexpected box-model answer: {answer:?}"), }; - assert!((second_width - 120.0).abs() <= 0.05, "{second_width}"); - assert_eq!(after_second.2, after_first.2); - assert_eq!(after_second.3, after_first.3); - assert_eq!(second.metrics, first.metrics); + assert!((second_width - 180.0).abs() <= 0.05, "{second_width}"); + assert_eq!(after_second.2, after_first.2 + second.metrics.elapsed); + assert_eq!(after_second.3, Some(second.metrics)); + assert_eq!(second.metrics.reason, moli_layout::LayoutFlushReason::SynchronousGeometry); + assert_eq!(second.metrics.paint_operation_count, 0); assert!(matches!( second.answers[0], moli_layout::LayoutQueryAnswer::DocumentMetrics(metrics) @@ -269,10 +279,10 @@ document.body.innerHTML = '
< .vm() .layout_snapshot_cache_observability_for_test(); assert!(!after_screenshot.0); - assert_eq!(after_screenshot.1, before.1 + 2); + assert_eq!(after_screenshot.1, before.1 + 3); assert_eq!(cache_after_screenshot.0, cache_before.0 + 1); - assert_eq!(cache_after_screenshot.1, cache_before.1 + 1); - assert_eq!(cache_after_screenshot.2, cache_before.2 + 2); + assert_eq!(cache_after_screenshot.1, cache_before.1 + 2); + assert_eq!(cache_after_screenshot.2, cache_before.2 + 3); let (_, retention) = cache_after_screenshot .3 .expect("fresh paint layout should publish its frozen tree"); @@ -303,10 +313,10 @@ document.body.innerHTML = '
< let cache_after_third = page_vm .vm() .layout_snapshot_cache_observability_for_test(); - assert_eq!(after_third.1, before.1 + 2); + assert_eq!(after_third.1, before.1 + 3); assert_eq!(cache_after_third.0, cache_before.0 + 2); - assert_eq!(cache_after_third.1, cache_before.1 + 1); - assert_eq!(cache_after_third.2, cache_before.2 + 2); + assert_eq!(cache_after_third.1, cache_before.1 + 2); + assert_eq!(cache_after_third.2, cache_before.2 + 3); let third_width = match &third.answers[1] { moli_layout::LayoutQueryAnswer::BoxModel(Some(model)) => { model.border.bounding_rect().width @@ -371,22 +381,22 @@ document.body.innerHTML = '
'; page_vm .vm_mut() .eval("document.getElementById('target').style.width='80px'; 'mutated'")?; - let stale = moli_layout::GeometryProvider::answer( + let before_next_frame = moli_layout::GeometryProvider::answer( page_vm.vm_mut(), moli_layout::LayoutFlushReason::SynchronousGeometry, moli_layout::LayoutViewport::new(320, 200, 1.0), &batch, )?; - let stale_width = match &stale.answers[0] { + let before_next_frame_width = match &before_next_frame.answers[0] { moli_layout::LayoutQueryAnswer::BoxModel(Some(model)) => { model.border.bounding_rect().width } - answer => panic!("unexpected stale box-model answer: {answer:?}"), + answer => panic!("unexpected box-model answer before the next frame: {answer:?}"), }; - assert!((stale_width - 40.0).abs() <= 0.05, "{stale_width}"); + assert!((before_next_frame_width - 80.0).abs() <= 0.05, "{before_next_frame_width}"); assert_eq!( page_vm.vm().layout_pass_observability_for_test().1, - passes_before + 1 + passes_before + 2 ); page_vm @@ -398,14 +408,14 @@ document.body.innerHTML = '
'; .expect("second screencast frame layout"); assert_eq!( page_vm.vm().layout_pass_observability_for_test().1, - passes_before + 2 + passes_before + 3 ); let cache_after_second = page_vm .vm() .layout_snapshot_cache_observability_for_test(); - assert_eq!(cache_after_second.0, cache_before.0 + 1); - assert_eq!(cache_after_second.1, cache_before.1); - assert_eq!(cache_after_second.2, cache_before.2 + 2); + assert_eq!(cache_after_second.0, cache_before.0); + assert_eq!(cache_after_second.1, cache_before.1 + 1); + assert_eq!(cache_after_second.2, cache_before.2 + 3); assert!(cache_after_second.3.is_some()); let refreshed = moli_layout::GeometryProvider::answer( @@ -423,14 +433,14 @@ document.body.innerHTML = '
'; assert!((refreshed_width - 80.0).abs() <= 0.05, "{refreshed_width}"); assert_eq!( page_vm.vm().layout_pass_observability_for_test().1, - passes_before + 2 + passes_before + 3 ); let cache_after_query = page_vm .vm() .layout_snapshot_cache_observability_for_test(); - assert_eq!(cache_after_query.0, cache_before.0 + 2); - assert_eq!(cache_after_query.1, cache_before.1); - assert_eq!(cache_after_query.2, cache_before.2 + 2); + assert_eq!(cache_after_query.0, cache_before.0 + 1); + assert_eq!(cache_after_query.1, cache_before.1 + 1); + assert_eq!(cache_after_query.2, cache_before.2 + 3); Ok::<_, anyhow::Error>(()) }) .await diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs index 6456027d7b..c323408856 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs @@ -769,6 +769,60 @@ fn iframe_input_reuses_one_top_level_snapshot_without_parent_child_ping_pong() { assert_eq!(cached.0, vm.document_runtime.document_handle()); } +#[test] +fn repeated_native_clicks_target_newly_inserted_buttons() { + let mut vm = new_parsed_test_vm( + "https://repeated-native-click.test/", + "", + ); + vm.eval("globalThis.clicks = []").unwrap(); + for index in 0..3 { + vm.eval(&format!("globalThis.buttonIndex = {index}")) + .unwrap(); + let geometry = vm + .eval( + r#" + (() => { + globalThis.button = document.createElement('button'); + button.id = 'button-' + buttonIndex; + button.textContent = 'Activate'; + button.style.cssText = 'position:absolute;left:10px;top:10px;width:100px;height:40px'; + button.onclick = event => clicks.push([event.target.id, event.isTrusted]); + document.body.appendChild(button); + const rects = button.getClientRects(); + if (rects.length !== 1) throw Error('new button must have a client rect'); + const rect = rects[0]; + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + if (document.elementFromPoint(x, y) !== button) throw Error('new button must be the hit target'); + return JSON.stringify([x, y]); + })() + "#, + ) + .expect("newly connected button geometry must replace stale input geometry"); + let point: [f64; 2] = serde_json::from_str(&geometry).unwrap(); + for (event, button, buttons) in + [("mousemove", -1, 0), ("mousedown", 0, 1), ("mouseup", 0, 0)] + { + vm.dispatch_mouse_event_at_point( + point[0], + point[1], + event, + button, + Some(buttons), + 0.0, + 0.0, + ) + .expect("native click should reach the newly inserted button"); + } + vm.eval("button.remove()").unwrap(); + } + assert_eq!( + vm.eval("JSON.stringify(clicks)").unwrap(), + r#"[["button-0",true],["button-1",true],["button-2",true]]"#, + ); +} + #[test] fn iframe_wheel_batch_reuses_one_composite_snapshot_for_every_scroll_step() { let mut vm = new_storage_test_vm("https://iframe-wheel-snapshot.test/"); diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs index fa06260e0d..2bce48103f 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs @@ -8934,8 +8934,11 @@ for (let index = 0; index < 128; index++) { ); assert_eq!(layout_after_first.1, layout_before.1 + 1); assert_eq!(layout_after_repeated.1, layout_after_first.1); - assert_eq!(layout_after_second.1, layout_after_first.1); - assert_eq!(layout_after_stylesheet_mutation.1, layout_after_first.1); + assert_eq!(layout_after_second.1, layout_after_first.1 + 1); + assert_eq!( + layout_after_stylesheet_mutation.1, + layout_after_second.1 + 1 + ); assert_eq!( update_materializations_after_first.saturating_sub(update_materializations_before), 1, @@ -8980,7 +8983,7 @@ for (let index = 0; index < 128; index++) { } #[test] -fn inner_text_new_sources_wait_for_a_fresh_paint_layout() { +fn inner_text_new_sources_refresh_layout_before_paint() { let mut vm = new_parsed_test_vm( "https://inner-text-latest-layout.test/", "
a
", @@ -9007,30 +9010,30 @@ fn inner_text_new_sources_wait_for_a_fresh_paint_layout() { "const added = document.createElement('span'); added.textContent = 'b'; target.append(added); target.innerText", ) .expect("the warm innerText read should evaluate"), - "a", - "a text source absent from the latest frozen layout tree remains unrendered until refresh" + "ab", + "a connected text source must be rendered by the next synchronous innerText read" ); - assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 1); + assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 2); vm.screenshot_layout_snapshot(moli_layout::PaintViewport::new(320, 200, 1.0)) .expect("fresh paint layout should succeed") .expect("the fixture should have a layout root"); - assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 2); + assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 3); assert_eq!( vm.eval("target.innerText") .expect("innerText should read the refreshed geometry snapshot"), "ab" ); - assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 2); + assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 3); let cache_after = vm.layout_snapshot_cache_observability_for_test(); - assert_eq!(cache_after.0, cache_before.0 + 2); - assert_eq!(cache_after.1, cache_before.1 + 1); - assert_eq!(cache_after.2, cache_before.2 + 2); + assert_eq!(cache_after.0, cache_before.0 + 1); + assert_eq!(cache_after.1, cache_before.1 + 2); + assert_eq!(cache_after.2, cache_before.2 + 3); } #[test] -fn parser_connected_inner_text_bypasses_a_stale_layout_snapshot() { +fn inner_text_refreshes_during_and_after_parsing() { let mut vm = new_parsed_test_vm( "https://parser-inner-text-layout.test/", "
a
", @@ -9073,12 +9076,21 @@ fn parser_connected_inner_text_bypasses_a_stale_layout_snapshot() { "const addedAfterParser = document.createElement('span'); addedAfterParser.textContent = 'c'; target.append(addedAfterParser); target.innerText", ) .expect("post-parse innerText should evaluate"), - "ab", - "ordinary post-parse reads should keep the demand-driven snapshot contract" + "abc", + "ordinary post-parse reads must also render newly inserted text" ); assert_eq!( vm.layout_pass_observability_for_test().1, - passes_after_initial_read + 1 + passes_after_initial_read + 2 + ); + assert_eq!( + vm.eval("target.innerText") + .expect("unchanged innerText read"), + "abc" + ); + assert_eq!( + vm.layout_pass_observability_for_test().1, + passes_after_initial_read + 2 ); } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs index 981884adb2..6f9f3d5136 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs @@ -1615,7 +1615,7 @@ body.setHTMLUnsafe(` } #[test] -fn geometry_getters_reuse_latest_layout_across_nodes_and_mutation() { +fn geometry_getters_reuse_layout_until_dom_mutation() { let mut vm = new_storage_test_vm("https://oneshot-layout-demand.test/"); let passes_before = vm ._context_host @@ -1664,18 +1664,21 @@ fn geometry_getters_reuse_latest_layout_across_nodes_and_mutation() { ) .expect("latest layout snapshot reads should evaluate"); - assert_eq!(result, "38|8|38|8|38|38|0"); + assert_eq!(result, "38|8|38|8|38|43|8"); let passes = vm ._context_host .borrow() .layout_pass_observability_for_test() .1 .saturating_sub(passes_before); - assert_eq!(passes, 1, "only the cold getter may build layout"); + assert_eq!( + passes, 2, + "the first getter after mutation must refresh layout" + ); let cache_after = vm.layout_snapshot_cache_observability_for_test(); - assert_eq!(cache_after.0, cache_before.0 + 10); - assert_eq!(cache_after.1, cache_before.1 + 1); - assert_eq!(cache_after.2, cache_before.2 + 1); + assert_eq!(cache_after.0, cache_before.0 + 9); + assert_eq!(cache_after.1, cache_before.1 + 2); + assert_eq!(cache_after.2, cache_before.2 + 2); assert!(cache_after.3.is_some()); } @@ -1715,6 +1718,32 @@ fn switching_to_mock_geometry_drops_the_latest_real_layout_snapshot() { ); } +#[test] +fn geometry_getters_refresh_after_cssom_changes_without_dom_mutation() { + let mut vm = new_parsed_test_vm( + "https://layout-cssom-inputs.test/", + "
", + ); + assert_eq!(vm.eval("target.offsetWidth").unwrap(), "10"); + let passes_before = vm.layout_pass_observability_for_test().1; + let dom_version = vm._context_host.borrow().dom_host().dom_version(); + assert_eq!( + vm.eval("document.styleSheets[0].cssRules[0].style.width = '40px'; target.offsetWidth") + .unwrap(), + "40", + ); + assert_eq!( + vm._context_host.borrow().dom_host().dom_version(), + dom_version + ); + assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 1); + assert_eq!( + vm.eval("target.getBoundingClientRect().width").unwrap(), + "40" + ); + assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 1); +} + #[test] fn block_flow_geometry_includes_body_margin_for_following_nodes() { let mut vm = new_storage_test_vm("https://mock-flow-height-hint.test/");