fix(webidl): expose readonly ResizeObserver result snapshots

This commit is contained in:
ldm0
2026-09-28 00:57:59 +08:00
parent a47b30ae5e
commit 4d526d9bca
15 changed files with 529 additions and 46 deletions
@@ -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
@@ -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,
@@ -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(
@@ -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<ResizeObserverSizeDeclaration>,
#[webapi(data_property, enumerable)]
border_box_size: Vec<ResizeObserverSizeDeclaration>,
#[webapi(data_property, enumerable)]
device_pixel_content_box_size: Vec<ResizeObserverSizeDeclaration>,
}
#[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<ResizeObserverSizeDeclaration> {
vec![ResizeObserverSizeDeclaration {
inline_size,
block_size,
}]
}
fn throw_resize_observer_layout_error(
scope: &mut v8::PinScope<'_, '_>,
error: moli_layout::LayoutError,
@@ -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"),
);
}
@@ -1897,6 +1897,8 @@ pub(crate) fn finish_context_bootstrap(
("IntersectionObserver", "IntersectionObserver"),
("IntersectionObserverEntry", "IntersectionObserverEntry"),
("ResizeObserver", "ResizeObserver"),
("ResizeObserverEntry", "ResizeObserverEntry"),
("ResizeObserverSize", "ResizeObserverSize"),
("PerformanceObserver", "PerformanceObserver"),
(
"PerformanceObserverEntryList",
@@ -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,
@@ -107,6 +107,8 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
DomPoint,
DomMatrix,
ResizeObserver,
ResizeObserverEntry,
ResizeObserverSize,
PerformanceObserver,
PerformanceObserverEntryList,
PerformanceEntry,
@@ -324,6 +324,13 @@ pub(crate) struct PreparedObserverCallback {
}
impl PreparedObserverCallback {
pub(crate) fn relevant_context<'s>(
&self,
scope: &mut v8::PinScope<'s, '_>,
) -> Option<v8::Local<'s, v8::Context>> {
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))
@@ -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')
@@ -2160,3 +2160,5 @@ mod history_replace_forward;
mod extracted;
mod navigation_timing_inheritance;
mod resize_observer_entries;
@@ -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}");
}
}
@@ -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"
);
}
@@ -180,6 +180,16 @@ pub(crate) enum PreparedWindowWebIdlCallbackFunctionOutcome<R, E> {
}
impl PreparedWindowWebIdlCallbackFunction {
pub(crate) fn relevant_context<'s>(
&self,
scope: &mut v8::PinScope<'s, '_>,
) -> Option<v8::Local<'s, v8::Context>> {
match self {
Self::Live { callback, .. } => Some(callback.relevant_context(scope)),
Self::Retired => None,
}
}
pub(crate) const fn relevant_identity(&self) -> Option<WindowExecutionContextIdentity> {
match self {
Self::Live {
@@ -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;
})();