mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 08:00:59 +00:00
fix(webidl): expose readonly ResizeObserver result snapshots
This commit is contained in:
@@ -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;
|
||||
})();
|
||||
Reference in New Issue
Block a user