From f4d34a69aeeeee4dea6961fc8d158ca769b5d436 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 14 Sep 2026 00:50:01 +0800 Subject: [PATCH] fix(layout): refresh geometry after DOM and style changes Check the DOM, document style generations, viewport state, media environment, and visual resources before reusing layout for synchronous geometry or input. Keep snapshot reads available and reuse the tree while its inputs are unchanged. Cover repeated trusted clicks on newly inserted buttons and CSSOM-only changes. Update geometry, innerText, grid, and screencast tests for synchronous refresh, and record nine newly passing CloseWatcher WPT cases. --- .../wpt-cross-current/passed-cases.txt | 9 +++ .../src/native_bridge/context_host/layout.rs | 31 +++++++-- .../context_host/layout_state.rs | 20 ++++++ .../page_vm/tests/computed_size/sampling.rs | 15 +++-- .../tests/grid_resolved_track_values.rs | 16 ++++- .../runtime/page_vm/tests/rendering_update.rs | 64 +++++++++++-------- .../tests/dom_elements/dom_surface.rs | 54 ++++++++++++++++ .../src/script_vm/tests/dom_xhr/dom.rs | 42 +++++++----- .../src/script_vm/tests/dom_xhr/misc.rs | 41 ++++++++++-- 9 files changed, 232 insertions(+), 60 deletions(-) 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/");