diff --git a/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs b/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs index 770ade2050..39b7292d28 100644 --- a/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs +++ b/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs @@ -696,6 +696,8 @@ pub(in crate::context_bootstrap) fn build_constructor_template<'s>( .build(scope) } ConstructorKind::PerformanceEntry + | ConstructorKind::ResizeObserverEntry + | ConstructorKind::ResizeObserverSize | ConstructorKind::PerformanceNavigationTiming | ConstructorKind::PerformanceMeasure | ConstructorKind::PerformanceResourceTiming diff --git a/moli-renderer-v8/src/context_bootstrap/dom_rect.rs b/moli-renderer-v8/src/context_bootstrap/dom_rect.rs index c91bdb8861..a589726941 100644 --- a/moli-renderer-v8/src/context_bootstrap/dom_rect.rs +++ b/moli-renderer-v8/src/context_bootstrap/dom_rect.rs @@ -289,7 +289,7 @@ pub(crate) fn build_dom_rect_object<'s>( .expect("DOMRect declaration should bind") } -fn build_dom_rect_readonly_object<'s>( +pub(super) fn build_dom_rect_readonly_object<'s>( scope: &mut v8::PinScope<'s, '_>, x: f64, y: f64, diff --git a/moli-renderer-v8/src/context_bootstrap/observer_template.rs b/moli-renderer-v8/src/context_bootstrap/observer_template.rs index c30045731b..053021ed18 100644 --- a/moli-renderer-v8/src/context_bootstrap/observer_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/observer_template.rs @@ -152,6 +152,11 @@ pub(super) fn install_observer_template_bindings<'s>( let proto = template.prototype_template(scope); ResizeObserverTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); } + "ResizeObserverEntry" | "ResizeObserverSize" => { + super::resize_observer_runtime::install_resize_observer_entry_template_bindings( + scope, template, spec_name, + ); + } "PerformanceObserver" => { let proto = template.prototype_template(scope); PerformanceObserverTemplateMethodsDeclaration::initialize_prototype_template( 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 256335e282..cac4b94f5e 100644 --- a/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs @@ -1,3 +1,4 @@ +mod entry; use super::*; use crate::host::report_event_callback_exception; use crate::observer_runtime::ObserverCallbackId; @@ -5,6 +6,8 @@ use crate::util::{get_private_value, serialize_v8_iter_array, set_private_value} use crate::web_api_interfaces; use crate::webidl; use crate::window_webidl_callback::WindowWebIdlCallbackFunctionOutcome; +use entry::ResizeObserverEntryData; +pub(super) use entry::install_resize_observer_entry_template_bindings; use moli_webapi_declare::WebApiObject; #[derive(WebApiObject)] @@ -26,21 +29,6 @@ struct ResizeObserverObjectDeclaration<'s> { scheduled: (), } -#[derive(WebApiObject)] -#[webapi(interface = web_api_interfaces::ResizeObserverEntry, prototype = "Object")] -struct ResizeObserverEntryDeclaration<'scope> { - #[webapi(data_property, enumerable)] - target: v8::Local<'scope, v8::Value>, - #[webapi(data_property, enumerable)] - content_rect: v8::Local<'scope, v8::Object>, - #[webapi(data_property, enumerable)] - content_box_size: Vec, - #[webapi(data_property, enumerable)] - border_box_size: Vec, - #[webapi(data_property, enumerable)] - device_pixel_content_box_size: Vec, -} - #[derive(WebApiObject)] #[webapi(plain)] struct ResizeObserverObservedRecordDeclaration<'scope> { @@ -50,15 +38,6 @@ struct ResizeObserverObservedRecordDeclaration<'scope> { observed_box: String, } -#[derive(WebApiObject)] -#[webapi(interface = web_api_interfaces::ResizeObserverSize, prototype = "Object")] -struct ResizeObserverSizeDeclaration { - #[webapi(data_property, enumerable)] - inline_size: f64, - #[webapi(data_property, enumerable)] - block_size: f64, -} - #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "ResizeObserver")] struct ResizeObserverConstructorArgs { @@ -297,7 +276,14 @@ fn resize_observer_flush_callback<'s>( set_resize_observer_pending_targets(scope, observer, pending_targets); return; }; - let entries = match build_resize_observer_entries(scope, pending_targets) { + let Some(callback_context) = callback.relevant_context(scope) else { + return; + }; + let result = { + let scope = &mut v8::ContextScope::new(scope, callback_context); + build_resize_observer_entries(scope, pending_targets) + }; + let entries = match result { Ok(entries) => entries, Err(error) => { let pending_targets = v8::Array::new(scope, 0); @@ -494,10 +480,10 @@ fn build_resize_observer_entries<'s>( .as_ref() .map(|metrics| f64::from(metrics.offset_size.height)) .unwrap_or(0.0); - let rect = build_dom_rect_object(scope, 0.0, 0.0, content_width, content_height); - let content_box_size = resize_observer_box_size_list(content_width, content_height); - let border_box_size = resize_observer_box_size_list(border_width, border_height); - let device_pixel_content_box_size = resize_observer_box_size_list( + 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); + let device_pixel_content_box_size = ( (content_width * f64::from(dpr)).round(), (content_height * f64::from(dpr)).round(), ); @@ -519,15 +505,14 @@ fn build_resize_observer_entries<'s>( } set_resize_observer_last_reported_size(scope, record, observed_size); } - let entry = ResizeObserverEntryDeclaration { + let entry = ResizeObserverEntryData { target: entry.target, content_rect: rect, content_box_size, border_box_size, device_pixel_content_box_size, } - .bind(scope) - .expect("ResizeObserverEntry declaration should bind"); + .into_object(scope); entries.push(entry); } Ok(serialize_v8_iter_array(scope, entries).unwrap_or_else(|| v8::Array::new(scope, 0))) @@ -646,16 +631,6 @@ fn observed_record_index<'s>( }) } -fn resize_observer_box_size_list( - inline_size: f64, - block_size: f64, -) -> Vec { - vec![ResizeObserverSizeDeclaration { - inline_size, - block_size, - }] -} - fn throw_resize_observer_layout_error( scope: &mut v8::PinScope<'_, '_>, error: moli_layout::LayoutError, diff --git a/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime/entry.rs b/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime/entry.rs new file mode 100644 index 0000000000..2844fec892 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime/entry.rs @@ -0,0 +1,152 @@ +use crate::context_bootstrap::dom_rect::build_dom_rect_readonly_object; +use crate::util::{callback_data_index_value, callback_data_item, get_private_value}; +use crate::web_api_interfaces; +use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; + +const TARGET_SLOT: &str = "__moliResizeObserverEntryTarget"; +const CONTENT_RECT_SLOT: &str = "__moliResizeObserverEntryContentRect"; +const CONTENT_BOX_SIZE_SLOT: &str = "__moliResizeObserverEntryContentBoxSize"; +const BORDER_BOX_SIZE_SLOT: &str = "__moliResizeObserverEntryBorderBoxSize"; +const DEVICE_PIXEL_CONTENT_BOX_SIZE_SLOT: &str = + "__moliResizeObserverEntryDevicePixelContentBoxSize"; +const INLINE_SIZE_SLOT: &str = "__moliResizeObserverSizeInlineSize"; +const BLOCK_SIZE_SLOT: &str = "__moliResizeObserverSizeBlockSize"; + +const ATTRIBUTE_SLOTS: &[&str] = &[ + TARGET_SLOT, + CONTENT_RECT_SLOT, + CONTENT_BOX_SIZE_SLOT, + BORDER_BOX_SIZE_SLOT, + DEVICE_PIXEL_CONTENT_BOX_SIZE_SLOT, + INLINE_SIZE_SLOT, + BLOCK_SIZE_SLOT, +]; + +// Sampling retains native dimensions. JS wrappers are created only for active +// observations, in the callback's relevant realm when they are delivered. +pub(super) struct ResizeObserverEntryData<'s> { + pub(super) target: v8::Local<'s, v8::Value>, + pub(super) content_rect: (f64, f64, f64, f64), + pub(super) content_box_size: (f64, f64), + pub(super) border_box_size: (f64, f64), + pub(super) device_pixel_content_box_size: (f64, f64), +} + +impl<'s> ResizeObserverEntryData<'s> { + pub(super) fn into_object(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object> { + let (x, y, width, height) = self.content_rect; + let content_rect = build_dom_rect_readonly_object(scope, x, y, width, height); + let content_box_size = frozen_box_size(scope, self.content_box_size); + let border_box_size = frozen_box_size(scope, self.border_box_size); + let device_pixel_content_box_size = + frozen_box_size(scope, self.device_pixel_content_box_size); + ResizeObserverEntryDeclaration { + target: self.target, + content_rect, + content_box_size, + border_box_size, + device_pixel_content_box_size, + } + .bind(scope) + .expect("ResizeObserverEntry declaration should bind") + } +} + +#[derive(WebApiObject)] +#[webapi(interface = web_api_interfaces::ResizeObserverEntry)] +struct ResizeObserverEntryDeclaration<'s> { + #[webapi(slot = TARGET_SLOT)] + target: v8::Local<'s, v8::Value>, + #[webapi(slot = CONTENT_RECT_SLOT)] + content_rect: v8::Local<'s, v8::Object>, + #[webapi(slot = CONTENT_BOX_SIZE_SLOT)] + content_box_size: v8::Local<'s, v8::Array>, + #[webapi(slot = BORDER_BOX_SIZE_SLOT)] + border_box_size: v8::Local<'s, v8::Array>, + #[webapi(slot = DEVICE_PIXEL_CONTENT_BOX_SIZE_SLOT)] + device_pixel_content_box_size: v8::Local<'s, v8::Array>, +} + +#[derive(WebApiObject)] +#[webapi(interface = web_api_interfaces::ResizeObserverSize)] +struct ResizeObserverSizeDeclaration { + #[webapi(slot = INLINE_SIZE_SLOT)] + inline_size: f64, + #[webapi(slot = BLOCK_SIZE_SLOT)] + block_size: f64, +} + +fn frozen_box_size<'s>( + scope: &mut v8::PinScope<'s, '_>, + (inline_size, block_size): (f64, f64), +) -> v8::Local<'s, v8::Array> { + let size = ResizeObserverSizeDeclaration::new(inline_size, block_size) + .bind(scope) + .expect("ResizeObserverSize declaration should bind"); + let array = v8::Array::new_with_elements(scope, &[size.into()]); + assert_eq!( + array.set_integrity_level(scope, v8::IntegrityLevel::Frozen), + Some(true), + "new ResizeObserver box-size array should freeze" + ); + array +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::ResizeObserverEntry, enumerable, receiver)] +struct ResizeObserverEntryPrototypeDeclaration { + #[webapi(accessor_property, getter = result_attribute_getter, data = callback_data_index_value(scope, 0))] + target: (), + #[webapi(accessor_property, getter = result_attribute_getter, data = callback_data_index_value(scope, 1))] + content_rect: (), + #[webapi(accessor_property, getter = result_attribute_getter, data = callback_data_index_value(scope, 2))] + content_box_size: (), + #[webapi(accessor_property, getter = result_attribute_getter, data = callback_data_index_value(scope, 3))] + border_box_size: (), + #[webapi(accessor_property, getter = result_attribute_getter, data = callback_data_index_value(scope, 4))] + device_pixel_content_box_size: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::ResizeObserverSize, enumerable, receiver)] +struct ResizeObserverSizePrototypeDeclaration { + #[webapi(accessor_property, getter = result_attribute_getter, data = callback_data_index_value(scope, 5))] + inline_size: (), + #[webapi(accessor_property, getter = result_attribute_getter, data = callback_data_index_value(scope, 6))] + block_size: (), +} + +pub(in crate::context_bootstrap) fn install_resize_observer_entry_template_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, + name: &str, +) { + let prototype = template.prototype_template(scope); + match name { + "ResizeObserverEntry" => { + ResizeObserverEntryPrototypeDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "ResizeObserverSize" => { + ResizeObserverSizePrototypeDeclaration::initialize_prototype_template(scope, prototype); + } + _ => {} + } +} + +fn result_attribute_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(slot) = + callback_data_item(scope, &args, ATTRIBUTE_SLOTS, "ResizeObserver result slots") + else { + return; + }; + rv.set( + get_private_value(scope, args.this(), slot) + .expect("branded ResizeObserver result should retain its attributes"), + ); +} diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs index b1a84af0f1..6f708c4df9 100644 --- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs @@ -1897,6 +1897,8 @@ pub(crate) fn finish_context_bootstrap( ("IntersectionObserver", "IntersectionObserver"), ("IntersectionObserverEntry", "IntersectionObserverEntry"), ("ResizeObserver", "ResizeObserver"), + ("ResizeObserverEntry", "ResizeObserverEntry"), + ("ResizeObserverSize", "ResizeObserverSize"), ("PerformanceObserver", "PerformanceObserver"), ( "PerformanceObserverEntryList", diff --git a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs index 19634ac72a..bde1b6d5ae 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs @@ -1003,6 +1003,14 @@ const CONSTRUCTOR_SPECS_AFTER_STREAMS: &[ConstructorSpec] = &[ interface: web_api_interfaces::ResizeObserver::DESCRIPTOR, kind: ConstructorKind::ResizeObserver, }, + ConstructorSpec { + interface: web_api_interfaces::ResizeObserverEntry::DESCRIPTOR, + kind: ConstructorKind::ResizeObserverEntry, + }, + ConstructorSpec { + interface: web_api_interfaces::ResizeObserverSize::DESCRIPTOR, + kind: ConstructorKind::ResizeObserverSize, + }, ConstructorSpec { interface: web_api_interfaces::PerformanceObserver::DESCRIPTOR, kind: ConstructorKind::PerformanceObserver, diff --git a/moli-renderer-v8/src/context_bootstrap/specs/types.rs b/moli-renderer-v8/src/context_bootstrap/specs/types.rs index faa534fbde..cbc4c95f2f 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/types.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/types.rs @@ -107,6 +107,8 @@ pub(in crate::context_bootstrap) enum ConstructorKind { DomPoint, DomMatrix, ResizeObserver, + ResizeObserverEntry, + ResizeObserverSize, PerformanceObserver, PerformanceObserverEntryList, PerformanceEntry, diff --git a/moli-renderer-v8/src/observer_runtime/callback.rs b/moli-renderer-v8/src/observer_runtime/callback.rs index 9655634afe..f1c9666552 100644 --- a/moli-renderer-v8/src/observer_runtime/callback.rs +++ b/moli-renderer-v8/src/observer_runtime/callback.rs @@ -324,6 +324,13 @@ pub(crate) struct PreparedObserverCallback { } impl PreparedObserverCallback { + pub(crate) fn relevant_context<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + ) -> Option> { + self.callback.relevant_context(scope) + } + pub(crate) fn is_current(&self, host: &JsContextHost) -> bool { self.observer_identity .is_some_and(|identity| host.window_execution_context_identity_is_current(identity)) 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 3a2ea2cd98..ddbc473780 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 @@ -1720,7 +1720,7 @@ fn geometry_rect_objects_expose_domrect_to_json() { rangeRect instanceof DOMRect, typeof rangeRect.toJSON, rangeRects[0] instanceof DOMRect, - contentRect instanceof DOMRect, + Object.getPrototypeOf(contentRect) === DOMRectReadOnly.prototype, typeof contentRect.toJSON, Object.hasOwn(DOMRectReadOnly.prototype, 'toJSON'), Object.hasOwn(DOMRect.prototype, 'toJSON') diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 4d4340fcbd..a4b168641f 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2160,3 +2160,5 @@ mod history_replace_forward; mod extracted; mod navigation_timing_inheritance; + +mod resize_observer_entries; 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 new file mode 100644 index 0000000000..1bc4fafb95 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/resize_observer_entries.rs @@ -0,0 +1,56 @@ +use super::*; + +#[tokio::test(flavor = "current_thread")] +async fn resize_observer_entries_preserve_webidl_snapshots_and_callback_realms() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + let mut vm = new_storage_page_task_executor_test_vm("https://resize-entries.test/"); + let count: usize = vm + .eval(include_str!( + "../../../tests/fixtures/resize-observer-entry-webidl.js" + )) + .unwrap() + .parse() + .unwrap(); + assert_eq!(count, 12); + vm.eval( + "__resizeEntryChecks.resumeAfterLayout = null; + __resizeEntryChecks.publishLayout = () => new Promise(resolve => { + __resizeEntryChecks.resumeAfterLayout = resolve; + });", + ) + .unwrap(); + for index in 0..count { + let name = vm + .eval(&format!("__resizeEntryChecks.start({index})")) + .unwrap(); + let mut publications = 0; + loop { + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__resizeEntryChecks.result !== null || __resizeEntryChecks.resumeAfterLayout !== null)", + "true", + &name, + ) + .await; + if vm.eval("__resizeEntryChecks.result !== null").unwrap() == "true" { + break; + } + publications += 1; + assert!(publications <= 2, "{name}: unexpected layout request"); + vm.publish_layout_for_test().unwrap(); + vm.eval( + "(() => { + const resume = __resizeEntryChecks.resumeAfterLayout; + __resizeEntryChecks.resumeAfterLayout = null; + resume(); + })()", + ) + .unwrap(); + } + let result = vm.eval("__resizeEntryChecks.result").unwrap(); + let result: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert!(result["total"].as_u64().unwrap() > 0, "{name}"); + assert_eq!(result["failures"], serde_json::json!([]), "{name}"); + } +} diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_fetch/byte_streams_and_observers.rs b/moli-renderer-v8/src/script_vm/tests/webidl_fetch/byte_streams_and_observers.rs index fba0a55dab..a4ed24f604 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch/byte_streams_and_observers.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch/byte_streams_and_observers.rs @@ -1652,7 +1652,12 @@ yield; // Publish this scene before reading its geometry. first.borderBoxSize[0].blockSize, second.devicePixelContentBoxSize[0].inlineSize, second.devicePixelContentBoxSize[0].blockSize, - Object.keys(first.contentBoxSize[0]).join(',') + Object.keys(first.contentBoxSize[0]).length, + Object.keys(ResizeObserverSize.prototype).join(','), + first instanceof ResizeObserverEntry, + first.contentBoxSize[0] instanceof ResizeObserverSize, + ['contentBoxSize', 'borderBoxSize', 'devicePixelContentBoxSize'] + .every(name => Object.isFrozen(first[name])) ].join('|'); })() "#, @@ -1661,7 +1666,7 @@ yield; // Publish this scene before reading its geometry. assert_eq!( result, - "true|true|true|true|1|41|23|41|23|41|23|inlineSize,blockSize" + "true|true|true|true|1|41|23|41|23|41|23|0|inlineSize,blockSize|true|true|true" ); } diff --git a/moli-renderer-v8/src/window_webidl_callback.rs b/moli-renderer-v8/src/window_webidl_callback.rs index f94bc83fba..0434f75796 100644 --- a/moli-renderer-v8/src/window_webidl_callback.rs +++ b/moli-renderer-v8/src/window_webidl_callback.rs @@ -180,6 +180,16 @@ pub(crate) enum PreparedWindowWebIdlCallbackFunctionOutcome { } impl PreparedWindowWebIdlCallbackFunction { + pub(crate) fn relevant_context<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + ) -> Option> { + match self { + Self::Live { callback, .. } => Some(callback.relevant_context(scope)), + Self::Retired => None, + } + } + pub(crate) const fn relevant_identity(&self) -> Option { match self { Self::Live { diff --git a/moli-renderer-v8/tests/fixtures/resize-observer-entry-webidl.js b/moli-renderer-v8/tests/fixtures/resize-observer-entry-webidl.js new file mode 100644 index 0000000000..d37815a776 --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/resize-observer-entry-webidl.js @@ -0,0 +1,257 @@ +(() => { + const assert = (condition, message) => { if (!condition) throw new Error(message); }; + const entryAttributes = ['target', 'contentRect', 'contentBoxSize', 'borderBoxSize', 'devicePixelContentBoxSize']; + const boxAttributes = entryAttributes.slice(2); + const sizeAttributes = ['inlineSize', 'blockSize']; + const withFrame = async run => { + const frame = document.body.appendChild(document.createElement('iframe')); + try { await run(frame.contentWindow); } + finally { frame.remove(); } + }; + const withEntry = async (creator, callbackRealm, targetRealm, run) => { + const target = targetRealm.document.body.appendChild(targetRealm.document.createElement('div')); + target.style.cssText = 'width:31px;height:17px;padding:2px;border:1px solid'; + target.getBoundingClientRect(); + let observer; + try { + const delivery = await new Promise(resolve => { + const callback = callbackRealm.Function('deliver', `return function(entries, observer) { + deliver({entry: entries[0], entries, observer, receiver: this}); + };`)(resolve); + observer = new creator.ResizeObserver(callback); + observer.observe(target); + }); + observer.disconnect(); + await run(delivery, target, observer); + } finally { observer?.disconnect(); target.remove(); } + }; + const cases = []; + for (const creatorName of ['main', 'child']) for (const callbackName of ['main', 'child']) { + for (const targetName of ['main', 'child']) { + cases.push({ + name: `entry from ${creatorName} observer, ${callbackName} callback, ${targetName} target`, + run: check => withFrame(other => { + const realms = {main: window, child: other}; + const owner = realms[callbackName]; + return withEntry(realms[creatorName], owner, realms[targetName], ({entry, entries, observer, receiver}, target, expectedObserver) => { + const size = entry.contentBoxSize[0]; + check('callback arguments retain identities and use the callback Array', () => { + assert(observer === expectedObserver && receiver === observer, 'observer argument and callback this'); + assert(Object.getPrototypeOf(entries) === owner.Array.prototype, 'callback sequence realm'); + assert(!Object.isFrozen(entries), 'sequence remains mutable'); + assert(entry.target === target, 'observed target identity'); + }); + for (const [name, value, attributes] of [ + ['ResizeObserverEntry', entry, entryAttributes], + ['ResizeObserverSize', size, sizeAttributes], + ]) { + check(`${name} native wrapper shape`, () => { + assert(Object.getPrototypeOf(value) === owner[name].prototype, 'callback realm prototype'); + assert(Object.prototype.toString.call(value) === `[object ${name}]`, 'interface toStringTag'); + assert(Object.getOwnPropertyNames(value).length === 0, 'attributes are not own properties'); + assert(Object.isExtensible(value), 'native wrapper remains extensible'); + }); + for (const attribute of attributes) { + check(`${name}.${attribute} is readonly`, () => { + const saved = value[attribute]; + const written = Reflect.set(value, attribute, null); + if (written) Reflect.set(value, attribute, saved); + assert(!written, 'assignment must fail'); + assert(value[attribute] === saved, 'assignment must retain the snapshot'); + }); + for (const [calleeName, realm] of Object.entries(realms)) { + const label = `${calleeName} ${name}.${attribute}`; + const getter = () => Object.getOwnPropertyDescriptor(realm[name].prototype, attribute).get; + check(`${label} descriptor and genuine receiver`, () => { + const descriptor = Object.getOwnPropertyDescriptor(realm[name].prototype, attribute); + assert(descriptor.enumerable && descriptor.configurable && descriptor.set === undefined, 'readonly WebIDL descriptor'); + assert(descriptor.get.length === 0 && descriptor.get.name === `get ${attribute}`, 'getter signature'); + assert(Object.getPrototypeOf(descriptor.get) === realm.Function.prototype, 'getter realm'); + assert(getter().call(value) === value[attribute], 'borrowed getter retains native value'); + }); + let reads = 0; + const poison = () => { reads++; throw new Error('author trap ran'); }; + const revoked = Proxy.revocable(value, {}); revoked.revoke(); + const fake = Object.create(Object.getPrototypeOf(value)); + Object.defineProperty(fake, attribute, {get: poison}); + const invalid = [null, undefined, {}, Object.getPrototypeOf(value), Object.create(value), + Object.create(Object.getPrototypeOf(value), Object.getOwnPropertyDescriptors(value)), + new Proxy(value, {get: poison, getPrototypeOf: poison, has: poison}), revoked.proxy, + name === 'ResizeObserverEntry' ? size : entry, fake]; + invalid.forEach((receiver, index) => check(`${label} rejects receiver ${index}`, () => { + const get = getter(); + reads = 0; + let error; + try { get.call(receiver); } catch (caught) { error = caught; } + assert(error instanceof realm.TypeError, 'binding realm TypeError'); + assert(reads === 0, 'brand validation must not invoke author code'); + })); + check(`${label} survives public shadowing and prototype mutation`, () => { + const get = getter(), original = get.call(value), prototype = Object.getPrototypeOf(value); + reads = 0; + try { + Object.defineProperty(value, attribute, {get: poison, configurable: true}); + Object.setPrototypeOf(value, null); + assert(get.call(value) === original && reads === 0, 'private state is independent of public properties'); + } finally { + Object.setPrototypeOf(value, prototype); + Reflect.deleteProperty(value, attribute); + } + }); + } + } + check(`${name} cannot be structured cloned`, () => { + let error; + try { structuredClone(value); } catch (caught) { error = caught; } + assert(error instanceof DOMException && error.name === 'DataCloneError', 'native interface has no clone codec'); + }); + } + check('contentRect is a readonly native snapshot in the callback realm', () => { + const rect = entry.contentRect; + assert(Object.getPrototypeOf(rect) === owner.DOMRectReadOnly.prototype, 'DOMRectReadOnly realm and type'); + assert(Object.getOwnPropertyNames(rect).length === 0, 'private geometry'); + for (const key of ['x', 'y', 'width', 'height']) { + const original = rect[key]; + const written = Reflect.set(rect, key, 900); + if (written) Reflect.set(rect, key, original); + assert(!written && rect[key] === original, `${key} is readonly`); + } + }); + for (const attribute of boxAttributes) check(`${attribute} is a stable FrozenArray of native sizes`, () => { + const array = entry[attribute], first = array[0]; + assert(Object.getPrototypeOf(array) === owner.Array.prototype, 'array realm'); + assert(array === entry[attribute] && array.length === 1, 'stable snapshot array'); + assert(Object.isFrozen(array), 'FrozenArray'); + assert(Object.getPrototypeOf(first) === owner.ResizeObserverSize.prototype, 'size realm'); + assert(!Reflect.set(array, '0', null) && !Reflect.set(array, 'length', 0), 'index and length are readonly'); + assert(!Reflect.deleteProperty(array, '0') && !Reflect.defineProperty(array, 'extra', {value: 1}), 'array cannot shrink or grow'); + assert(array[0] === first && array.length === 1, 'failed changes retain contents'); + }); + check('box snapshots are independent objects', () => { + const arrays = boxAttributes.map(attribute => entry[attribute]); + assert(new Set(arrays).size === 3 && new Set(arrays.map(array => array[0])).size === 3, 'boxes must not share mutable wrappers'); + }); + }); + }) + }); + } + } + for (const owner of ['main', 'child']) cases.push({ + name: `${owner} interface exposure and intrinsic factories`, + run: check => withFrame(async other => { + const realm = owner === 'main' ? window : other; + for (const name of ['ResizeObserverEntry', 'ResizeObserverSize']) { + check(`${name} interface object`, () => { + const constructor = realm[name]; + assert(typeof constructor === 'function' && constructor.name === name && constructor.length === 0, 'interface function'); + assert(Object.getPrototypeOf(constructor.prototype) === realm.Object.prototype, 'interface prototype parent'); + assert(constructor.prototype.constructor === constructor, 'prototype constructor'); + const tag = Object.getOwnPropertyDescriptor(constructor.prototype, Symbol.toStringTag); + assert(tag.value === name && !tag.writable && !tag.enumerable && tag.configurable, 'toStringTag descriptor'); + for (const call of [() => constructor(), () => new constructor()]) { + let error; + try { call(); } catch (caught) { error = caught; } + assert(error instanceof realm.TypeError, 'no public construction algorithm'); + } + }); + } + const names = ['ResizeObserverEntry', 'ResizeObserverSize', 'DOMRectReadOnly', 'Array']; + const saved = names.map(name => [name, realm[name], Object.getOwnPropertyDescriptor(realm, name)]); + let reads = 0; + try { + for (const name of names) Object.defineProperty(realm, name, { + configurable: true, get() { reads++; throw new Error('public constructor getter ran'); } + }); + await withEntry(realm, realm, realm, ({entry, entries}) => check('factories use intrinsics after global shadowing', () => { + const originals = Object.fromEntries(saved.map(([name, value]) => [name, value])); + assert(Object.getPrototypeOf(entry) === originals.ResizeObserverEntry.prototype, 'entry intrinsic prototype'); + assert(Object.getPrototypeOf(entry.contentRect) === originals.DOMRectReadOnly.prototype, 'rect intrinsic prototype'); + assert(Object.getPrototypeOf(entry.contentBoxSize[0]) === originals.ResizeObserverSize.prototype, 'size intrinsic prototype'); + assert(Object.getPrototypeOf(entry.contentBoxSize) === originals.Array.prototype, 'array intrinsic prototype'); + assert(Object.getPrototypeOf(entries) === originals.Array.prototype && reads === 0, 'author constructors are not consulted'); + })); + } finally { + for (const [name, , descriptor] of saved) { + if (descriptor) Object.defineProperty(realm, name, descriptor); else Reflect.deleteProperty(realm, name); + } + } + }) + }); + cases.push({ + name: 'successive deliveries keep retained snapshots independent', + async run(check) { + const target = document.body.appendChild(document.createElement('div')); + target.style.cssText = 'width:31px;height:17px'; + let deliver; + const observer = new ResizeObserver(entries => deliver(entries[0])); + const sample = async () => { + // The native fixture publishes layout explicitly; browser runs can + // use the next rendering opportunity. + await (__resizeEntryChecks.publishLayout?.() ?? new Promise(requestAnimationFrame)); + return new Promise(resolve => { + deliver = resolve; + observer.observe(target); + }); + }; + try { + const first = await sample(); + assert(first, 'initial notification'); + observer.unobserve(target); + target.style.width = '47px'; + const second = await sample(); + check('old dimensions and object identities survive the resize', () => { + assert(first !== second, 'a new observation creates a fresh snapshot'); + assert(first.contentRect.width === 31 && first.contentBoxSize[0].inlineSize === 31, 'old dimensions'); + assert(second.contentRect.width === 47 && second.contentBoxSize[0].inlineSize === 47, 'new dimensions'); + for (const attribute of ['contentRect', ...boxAttributes]) assert(first[attribute] !== second[attribute], 'fresh nested snapshot'); + }); + observer.disconnect(); target.remove(); + check('snapshot remains readable after observer disconnect and target removal', () => { + assert(first.target === target && second.target === target, 'target identity remains'); + assert(first.contentRect.width === 31 && second.contentRect.width === 47, 'retained dimensions'); + }); + } finally { observer.disconnect(); target.remove(); } + } + }); + cases.push({ + name: 'Window-only result interfaces remain absent from Dedicated Workers', + async run(check) { + const source = `postMessage(['ResizeObserver', 'ResizeObserverEntry', 'ResizeObserverSize'].map(name => typeof globalThis[name]).join(','));`; + const url = URL.createObjectURL(new Blob([source], {type: 'text/javascript'})); + const worker = new Worker(url); + try { + const result = await new Promise((resolve, reject) => { + worker.onmessage = event => resolve(event.data); + worker.onerror = event => reject(new Error(event.message)); + }); + check('exposure set', () => assert(result === 'undefined,undefined,undefined', result)); + } finally { worker.terminate(); URL.revokeObjectURL(url); } + } + }); + globalThis.__resizeEntryChecks = { + result: null, + start(index) { + this.result = null; + const rows = []; + const check = (name, run) => { + try { run(); rows.push({name, passed: true}); } + catch (error) { rows.push({name, passed: false, error: String(error)}); } + }; + const finish = () => { this.result = JSON.stringify({total: rows.length, failures: rows.filter(row => !row.passed)}); }; + this.pending = cases[index].run(check).then(finish, error => { + check('scenario completes', () => { throw error; }); finish(); + }); + return cases[index].name; + }, + async all() { + const results = []; + for (let index = 0; index < cases.length; index++) { + const name = this.start(index); + await this.pending; + results.push({name, ...JSON.parse(this.result)}); + } + return JSON.stringify(results); + } + }; + return cases.length; +})();