mirror of
https://github.com/lexmount/moli.git
synced 2026-11-06 16:07:48 +00:00
fix(layout): refresh realtime iframe viewport queries
This commit is contained in:
@@ -5,6 +5,9 @@ use super::{
|
||||
};
|
||||
use crate::context_bootstrap::DEFAULT_WINDOW_SURFACE_PROFILE;
|
||||
use crate::context_bootstrap::current_window_style_viewport;
|
||||
use crate::context_bootstrap::window_accessors::{
|
||||
prepare_window_viewport_read, window_current_dispatch_scope,
|
||||
};
|
||||
use crate::context_bootstrap::{
|
||||
MEDIA_QUERY_LIST_LISTENERS_SLOT, MEDIA_QUERY_LIST_MATCHES_SLOT, MEDIA_QUERY_LIST_MEDIA_SLOT,
|
||||
MEDIA_QUERY_LIST_ONCHANGE_SLOT, MEDIA_QUERY_LIST_REGISTRY_SLOT,
|
||||
@@ -95,8 +98,18 @@ fn media_query_list_attribute_getter_callback<'s>(
|
||||
.unwrap_or_else(|| scope.get_current_context());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let media = media_query_list_media_slot(scope, args.this()).unwrap_or_default();
|
||||
let global = context.global(scope);
|
||||
let matches = if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
|
||||
let host = unsafe { &*host_ptr };
|
||||
// A retained list must not observe the replacement document that
|
||||
// now occupies the same iframe element's browsing context slot.
|
||||
if window_current_dispatch_scope(scope, global, host).is_none() {
|
||||
rv.set_bool(false);
|
||||
return;
|
||||
}
|
||||
if !prepare_window_viewport_read(scope, global) {
|
||||
return;
|
||||
}
|
||||
evaluate_match_media_query_list_with_viewport(
|
||||
&media,
|
||||
Some(host.emulated_media()),
|
||||
@@ -164,6 +177,10 @@ pub(crate) fn window_match_media_callback<'s>(
|
||||
return;
|
||||
};
|
||||
let serialized_query = serialize_match_media_query_list(&parsed.query);
|
||||
let global = scope.get_current_context().global(scope);
|
||||
if !prepare_window_viewport_read(scope, global) {
|
||||
return;
|
||||
}
|
||||
let (emulated_media, viewport) =
|
||||
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
|
||||
let host = unsafe { &*host_ptr };
|
||||
|
||||
@@ -134,18 +134,36 @@ fn visual_viewport_attribute_getter_callback<'s>(
|
||||
if matches!(
|
||||
slot,
|
||||
VISUAL_VIEWPORT_WIDTH_SLOT | VISUAL_VIEWPORT_HEIGHT_SLOT
|
||||
) && let Some(host_ptr) = crate::util::context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
let viewport =
|
||||
crate::context_bootstrap::current_window_style_viewport(scope, unsafe { &*host_ptr });
|
||||
) {
|
||||
// A borrowed getter belongs to its receiver's Window, even when the
|
||||
// function was obtained from another realm's VisualViewport prototype.
|
||||
let context = args
|
||||
.this()
|
||||
.get_creation_context(scope)
|
||||
.unwrap_or_else(|| scope.get_current_context());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let window = context.global(scope);
|
||||
let current = super::super::window_accessors::window_host_ptr(scope, window).is_some_and(
|
||||
|host_ptr| {
|
||||
super::super::window_accessors::window_current_dispatch_scope(
|
||||
scope,
|
||||
window,
|
||||
unsafe { &*host_ptr },
|
||||
)
|
||||
.is_some()
|
||||
},
|
||||
);
|
||||
if !current {
|
||||
rv.set_double(0.0);
|
||||
return;
|
||||
}
|
||||
if !super::super::window_accessors::prepare_window_viewport_read(scope, window) {
|
||||
return;
|
||||
}
|
||||
let value = if slot == VISUAL_VIEWPORT_WIDTH_SLOT {
|
||||
viewport
|
||||
.width
|
||||
.unwrap_or(DEFAULT_WINDOW_SURFACE_PROFILE.inner_width)
|
||||
super::super::window_accessors::window_inner_surface_width(scope, window)
|
||||
} else {
|
||||
viewport
|
||||
.height
|
||||
.unwrap_or(DEFAULT_WINDOW_SURFACE_PROFILE.inner_height)
|
||||
super::super::window_accessors::window_inner_surface_height(scope, window)
|
||||
};
|
||||
rv.set_double(value);
|
||||
return;
|
||||
|
||||
@@ -28,21 +28,22 @@ pub(super) use document::{WINDOW_DOCUMENT_SLOT, window_document_getter_template}
|
||||
pub(crate) use document::{
|
||||
bind_current_child_window_document, retain_window_document_in_retired_realm,
|
||||
};
|
||||
pub(super) use helpers::window_child_context_handle;
|
||||
pub(crate) use helpers::{current_window_style_viewport, window_host_ptr};
|
||||
pub(super) use helpers::{window_child_context_handle, window_current_dispatch_scope};
|
||||
pub(super) use interceptors::{
|
||||
window_indexed_property_descriptor, window_indexed_property_enumerator,
|
||||
window_indexed_property_getter, window_indexed_property_query, window_named_property_getter,
|
||||
window_named_property_query,
|
||||
};
|
||||
pub(super) use surface::{
|
||||
prepare_window_viewport_read, window_custom_elements_getter, window_device_pixel_ratio_getter,
|
||||
window_frames_getter, window_inner_height_getter, window_inner_surface_height,
|
||||
window_inner_surface_width, window_inner_width_getter, window_navigator_getter,
|
||||
window_opener_setter, window_outer_height_getter, window_outer_width_getter,
|
||||
window_performance_getter, window_screen_getter, window_scroll_x_getter,
|
||||
window_scroll_y_getter, window_self_getter, window_speech_synthesis_getter,
|
||||
window_visual_viewport_getter, window_window_getter,
|
||||
};
|
||||
pub(crate) use surface::{
|
||||
window_closed_getter, window_opener_getter, window_parent_getter, window_top_getter,
|
||||
};
|
||||
pub(super) use surface::{
|
||||
window_custom_elements_getter, window_device_pixel_ratio_getter, window_frames_getter,
|
||||
window_inner_height_getter, window_inner_surface_height, window_inner_surface_width,
|
||||
window_inner_width_getter, window_navigator_getter, window_opener_setter,
|
||||
window_outer_height_getter, window_outer_width_getter, window_performance_getter,
|
||||
window_screen_getter, window_scroll_x_getter, window_scroll_y_getter, window_self_getter,
|
||||
window_speech_synthesis_getter, window_visual_viewport_getter, window_window_getter,
|
||||
};
|
||||
|
||||
@@ -106,7 +106,7 @@ pub(crate) fn window_host_ptr(
|
||||
.or_else(|| context_host_ptr_from_global_bridge(scope))
|
||||
}
|
||||
|
||||
pub(super) fn window_current_dispatch_scope<'s>(
|
||||
pub(in crate::context_bootstrap) fn window_current_dispatch_scope<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
host: &JsContextHost,
|
||||
|
||||
@@ -47,7 +47,7 @@ fn window_inner_surface_dimension<'s>(
|
||||
|
||||
/// A child Window's viewport is produced by its parent layout. Resolve that
|
||||
/// dependency before public reads, without entering any lifecycle callbacks.
|
||||
fn prepare_window_inner_surface_read<'s>(
|
||||
pub(in crate::context_bootstrap) fn prepare_window_viewport_read<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
@@ -201,7 +201,7 @@ pub(in crate::context_bootstrap) fn window_inner_width_getter<'s>(
|
||||
let Some(receiver) = window_receiver(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if !prepare_window_inner_surface_read(scope, receiver) {
|
||||
if !prepare_window_viewport_read(scope, receiver) {
|
||||
return;
|
||||
}
|
||||
let width = window_inner_surface_width(scope, receiver);
|
||||
@@ -216,7 +216,7 @@ pub(in crate::context_bootstrap) fn window_inner_height_getter<'s>(
|
||||
let Some(receiver) = window_receiver(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if !prepare_window_inner_surface_read(scope, receiver) {
|
||||
if !prepare_window_viewport_read(scope, receiver) {
|
||||
return;
|
||||
}
|
||||
let height = window_inner_surface_height(scope, receiver);
|
||||
|
||||
@@ -18,6 +18,98 @@ fn frame_fixture(policy: LayoutPolicy, width: u32, height: u32) -> StandaloneScr
|
||||
vm
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realtime_frame_viewport_reads_refresh_ancestors_and_use_the_receiver_realm() {
|
||||
let mut vm = new_parsed_test_vm("https://realtime-frame.test/", "<!doctype html><body>");
|
||||
vm.set_layout_policy(LayoutPolicy::Realtime);
|
||||
// Run the identical synchronous fixture against the release CDP entry and
|
||||
// Chromium. Neither screenshots nor lifecycle callbacks can refresh a read.
|
||||
let fixture = include_str!("../../../tests/fixtures/realtime-frame-viewport-reads.js");
|
||||
let result = vm.eval(&format!("JSON.stringify({fixture})")).unwrap();
|
||||
let result: serde_json::Value = serde_json::from_str(&result).unwrap();
|
||||
assert_eq!(result["failures"], serde_json::json!([]), "{result}");
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test()
|
||||
.3
|
||||
.unwrap()
|
||||
.paint_operation_count,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realtime_frame_viewport_reads_share_current_layout_and_propagate_failures() {
|
||||
for (read, expected) in [
|
||||
("String(query.matches)", "true"),
|
||||
("String(child.matchMedia('(width:400px)').matches)", "true"),
|
||||
("String(viewport.width)", "400"),
|
||||
("String(viewport.height)", "120"),
|
||||
] {
|
||||
let mut vm = frame_fixture(LayoutPolicy::Realtime, 320, 100);
|
||||
vm.eval("globalThis.viewport=child.visualViewport;globalThis.query=child.matchMedia('(width:400px)');frame.offsetWidth")
|
||||
.unwrap();
|
||||
let before = vm.layout_pass_observability_for_test().1;
|
||||
vm.eval("frame.style.width='400px';frame.style.height='120px'")
|
||||
.unwrap();
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.simulate_active_layout_pass_for_test(true);
|
||||
let result = vm.eval(read);
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.simulate_active_layout_pass_for_test(false);
|
||||
let error = result.unwrap_err();
|
||||
assert!(
|
||||
error.to_string().contains("Layout failed:"),
|
||||
"{read}: {error}"
|
||||
);
|
||||
assert_eq!(vm.layout_pass_observability_for_test().1, before);
|
||||
assert_eq!(vm.eval(read).unwrap(), expected, "{read}");
|
||||
assert_eq!(vm.layout_pass_observability_for_test().1, before + 1);
|
||||
assert_eq!(vm.eval(read).unwrap(), expected, "{read}");
|
||||
assert_eq!(vm.layout_pass_observability_for_test().1, before + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_reads_do_not_add_layout_to_legacy_modes_or_top_level_queries() {
|
||||
for policy in [
|
||||
LayoutPolicy::Mock,
|
||||
LayoutPolicy::OnDemand,
|
||||
LayoutPolicy::Realtime,
|
||||
] {
|
||||
let mut vm = frame_fixture(policy, 320, 100);
|
||||
vm.eval("globalThis.viewport=child.visualViewport;globalThis.query=child.matchMedia('(width:400px)');frame.offsetWidth")
|
||||
.unwrap();
|
||||
let before = vm.layout_pass_observability_for_test().1;
|
||||
vm.eval("frame.style.width='400px';document.body.style.width='800px'")
|
||||
.unwrap();
|
||||
// Root viewport sizes do not depend on a descendant's box. Reading
|
||||
// them or a root media query must not unnecessarily flush the tree.
|
||||
vm.eval("visualViewport.width;visualViewport.height;matchMedia('(min-width:1px)').matches")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test().1,
|
||||
before,
|
||||
"{policy:?}"
|
||||
);
|
||||
if policy != LayoutPolicy::Realtime {
|
||||
vm.eval("viewport.width;viewport.height;query.matches;child.matchMedia('(width:400px)').matches")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test().1,
|
||||
before,
|
||||
"{policy:?}"
|
||||
);
|
||||
assert!(
|
||||
!vm._context_host
|
||||
.borrow()
|
||||
.realtime_layout_allocated_for_test()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realtime_layout_zero_sized_frame_preserves_fixed_and_percentage_metrics() {
|
||||
// Chromium 145, child root overflow:hidden: an empty viewport clips
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
(() => {
|
||||
const rows = [], failures = [], callbacks = [];
|
||||
const check = (name, actual, expected) => {
|
||||
rows.push([name, actual]);
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) failures.push(name);
|
||||
};
|
||||
const frame = document.createElement('iframe');
|
||||
frame.style.cssText = 'width:320px;height:100px;border:0';
|
||||
document.body.append(frame);
|
||||
const child = frame.contentWindow;
|
||||
child.document.open();
|
||||
child.document.write('<!doctype html><html style="overflow:hidden"><body style="margin:0"></body>');
|
||||
child.document.close();
|
||||
const viewport = child.visualViewport;
|
||||
const query = child.matchMedia('(min-width: 400px)');
|
||||
query.onchange = () => callbacks.push('media');
|
||||
viewport.onresize = () => callbacks.push('viewport');
|
||||
requestAnimationFrame(() => callbacks.push('frame'));
|
||||
// Establish an old canonical layout before each first dirty read.
|
||||
frame.getBoundingClientRect();
|
||||
check('initial', [query.matches, viewport.width, viewport.height], [false, 320, 100]);
|
||||
frame.style.width = '400px';
|
||||
check('retained-query', query.matches, true);
|
||||
frame.style.width = '420px';
|
||||
check('retained-width', viewport.width, 420);
|
||||
frame.style.height = '140px';
|
||||
check('retained-height', viewport.height, 140);
|
||||
frame.style.width = '380px';
|
||||
check('new-query', child.matchMedia('(max-width: 390px)').matches, true);
|
||||
frame.style.width = '440px';
|
||||
const widthGetter = Object.getOwnPropertyDescriptor(VisualViewport.prototype, 'width').get;
|
||||
check('borrowed-width', widthGetter.call(viewport), 440);
|
||||
const childWidthGetter = Object.getOwnPropertyDescriptor(child.VisualViewport.prototype, 'width').get;
|
||||
check('borrowed-root', childWidthGetter.call(visualViewport), visualViewport.width);
|
||||
const matchesGetter = Object.getOwnPropertyDescriptor(MediaQueryList.prototype, 'matches').get;
|
||||
frame.style.width = '360px';
|
||||
check('borrowed-query', matchesGetter.call(query), false);
|
||||
const nestedFrame = child.document.createElement('iframe');
|
||||
nestedFrame.style.cssText = 'width:50%;height:50px;border:0';
|
||||
child.document.body.append(nestedFrame);
|
||||
const nested = nestedFrame.contentWindow;
|
||||
nested.document.open();
|
||||
nested.document.write('<!doctype html><html style="overflow:hidden"><body style="margin:0"></body>');
|
||||
nested.document.close();
|
||||
const nestedQuery = nested.matchMedia('(min-width: 200px)');
|
||||
const nestedViewport = nested.visualViewport;
|
||||
frame.getBoundingClientRect();
|
||||
frame.style.width = '400px';
|
||||
check('nested-query', nestedQuery.matches, true);
|
||||
frame.style.width = '480px';
|
||||
check('nested-width', nestedViewport.width, 240);
|
||||
check('clean', [nestedViewport.width, query.matches, viewport.width], [240, true, 480]);
|
||||
frame.style.display = 'none';
|
||||
frame.style.width = '540px';
|
||||
frame.style.height = '180px';
|
||||
check('hidden', [viewport.width, viewport.height, query.matches], [480, 140, true]);
|
||||
frame.style.display = 'block';
|
||||
check('shown', [viewport.width, viewport.height, query.matches], [540, 180, true]);
|
||||
frame.remove();
|
||||
check('removed', [viewport.width, viewport.height, query.matches], [0, 0, false]);
|
||||
document.body.append(frame);
|
||||
check('reattached-old-objects', [viewport.width, viewport.height, query.matches], [0, 0, false]);
|
||||
check('synchronous-callbacks', callbacks, []);
|
||||
return {rows, failures};
|
||||
})()
|
||||
Reference in New Issue
Block a user