From 6e78b7cd8ba717fd1ffeee6d1e7b3d35fa191b2a Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 12 Oct 2026 06:38:20 +0800 Subject: [PATCH] feat(media): bind native PictureInPicture event payloads --- moli-renderer-v8/src/context_bootstrap.rs | 3 + .../assets/constructor_templates.rs | 3 + .../assets/prototype_bindings.rs | 1 + .../src/context_bootstrap/events/value.rs | 45 +++++- .../context_bootstrap/picture_in_picture.rs | 106 ++++++++++++++ .../src/context_bootstrap/specs/registry.rs | 8 ++ .../src/context_bootstrap/specs/types.rs | 1 + moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + .../tests/picture_in_picture_interfaces.js | 99 +++++++++++++ .../tests/picture_in_picture_interfaces.rs | 134 ++++++++++++++++++ moli-renderer-v8/src/web_api_interfaces.rs | 2 + 11 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 moli-renderer-v8/src/context_bootstrap/picture_in_picture.rs create mode 100644 moli-renderer-v8/src/script_vm/tests/picture_in_picture_interfaces.js create mode 100644 moli-renderer-v8/src/script_vm/tests/picture_in_picture_interfaces.rs diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 128423d866..d2d7a922aa 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -28,6 +28,9 @@ mod form_data_runtime; mod geometry_runtime; mod history_mutation; mod history_runtime; +mod picture_in_picture; +#[cfg(test)] +pub(crate) use picture_in_picture::closed_window_for_test as closed_picture_in_picture_window_for_test; mod time_ranges; pub(crate) mod web_mcp; pub(crate) use history_runtime::state::bind_isolated_window_history_owner; 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 a9718ec3b2..55e627bff7 100644 --- a/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs +++ b/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs @@ -115,6 +115,9 @@ pub(in crate::context_bootstrap) fn build_constructor_template<'s>( } ConstructorKind::BlobEvent => build_value_event_template(scope, ValueEventKind::Blob), ConstructorKind::GamepadEvent => build_value_event_template(scope, ValueEventKind::Gamepad), + ConstructorKind::PictureInPictureEvent => { + build_value_event_template(scope, ValueEventKind::PictureInPicture) + } ConstructorKind::WebGLContextEvent => { build_value_event_template(scope, ValueEventKind::WebGlContext) } diff --git a/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs b/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs index f527a4a431..99ba84d8bb 100644 --- a/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs @@ -426,6 +426,7 @@ pub(super) fn install_constructor_template_bindings<'s>( } install_media_cue_template_bindings(scope, template, spec.interface.name()); install_media_source_template_bindings(scope, template, spec.interface.name()); + super::super::picture_in_picture::install(scope, template, spec.interface.name()); super::super::video_codecs::install_video_codec_template_bindings( scope, template, diff --git a/moli-renderer-v8/src/context_bootstrap/events/value.rs b/moli-renderer-v8/src/context_bootstrap/events/value.rs index 37af5c543d..9f2edbc972 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/value.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/value.rs @@ -19,6 +19,7 @@ const STATUS_MESSAGE_SLOT: &str = "__moliWebGlContextEventStatusMessage"; const TIMECODE_SLOT: &str = "__moliValueEventTimecode"; const TOOL_NAME_SLOT: &str = "__moliToolEventName"; const GAMEPAD_SLOT: &str = "__moliValueEventGamepad"; +const PICTURE_IN_PICTURE_WINDOW_SLOT: &str = "__moliPictureInPictureEventWindow"; #[derive(Clone, Copy)] pub(in crate::context_bootstrap) enum ValueEventKind { @@ -29,6 +30,7 @@ pub(in crate::context_bootstrap) enum ValueEventKind { ToolActivated, ToolCancel, Gamepad, + PictureInPicture, } impl ValueEventKind { @@ -41,12 +43,13 @@ impl ValueEventKind { Self::ToolActivated => "ToolActivatedEvent", Self::ToolCancel => "ToolCancelEvent", Self::Gamepad => "GamepadEvent", + Self::PictureInPicture => "PictureInPictureEvent", } } fn length(self) -> i32 { match self { - Self::Blob => 2, + Self::Blob | Self::PictureInPicture => 2, _ => 1, } } @@ -200,6 +203,22 @@ struct BlobEventInit<'s> { timecode: Option, } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::PictureInPictureEvent, enumerable, receiver)] +struct PictureInPictureEventPrototype { + #[webapi(accessor_property, getter = payload_getter, data = v8str(scope, PICTURE_IN_PICTURE_WINDOW_SLOT))] + picture_in_picture_window: (), +} + +#[derive(webidl::WebIdlDictionary)] +#[webidl(prefix = "PictureInPictureEventInit")] +struct PictureInPictureEventInit<'s> { + #[webidl(inherit)] + base: EventInit, + #[webidl(required, interface = web_api_interfaces::PictureInPictureWindow)] + picture_in_picture_window: v8::Local<'s, v8::Object>, +} + fn string_member<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, @@ -240,6 +259,9 @@ pub(in crate::context_bootstrap) fn install_value_event_template_bindings<'s>( "ToolCancelEvent" => { ToolCancelEventPrototypeDeclaration::initialize_prototype_template(scope, prototype) } + "PictureInPictureEvent" => { + PictureInPictureEventPrototype::initialize_prototype_template(scope, prototype) + } "GamepadEvent" => { GamepadEventPrototypeDeclaration::initialize_prototype_template(scope, prototype) } @@ -253,7 +275,9 @@ fn payload_getter<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, ) { let key = args.data().to_rust_string_lossy(scope); - if let Some(value) = event_private_value(scope, args.this(), &key) { + let target = moli_webapi_declare::web_api_object_target(scope, args.this()) + .expect("validated value event receiver"); + if let Some(value) = event_private_value(scope, target, &key) { rv.set(value); } } @@ -281,6 +305,7 @@ fn value_event_constructor<'s>( Some(4) => ValueEventKind::ToolActivated, Some(5) => ValueEventKind::ToolCancel, Some(6) => ValueEventKind::Gamepad, + Some(7) => ValueEventKind::PictureInPicture, _ => return, }; if !args.is_construct_call() { @@ -386,6 +411,22 @@ fn value_event_constructor<'s>( parsed.base.composed, ) } + ValueEventKind::PictureInPicture => { + let parsed = webidl::parse_dictionary_object::( + scope, dictionary, + )?; + set_event_private_value( + scope, + state, + PICTURE_IN_PICTURE_WINDOW_SLOT, + parsed.picture_in_picture_window.into(), + ); + ( + parsed.base.bubbles, + parsed.base.cancelable, + parsed.base.composed, + ) + } ValueEventKind::Gamepad => { let parsed = webidl::parse_dictionary_object::(scope, dictionary)?; diff --git a/moli-renderer-v8/src/context_bootstrap/picture_in_picture.rs b/moli-renderer-v8/src/context_bootstrap/picture_in_picture.rs new file mode 100644 index 0000000000..b056dfcd04 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/picture_in_picture.rs @@ -0,0 +1,106 @@ +//! Picture-in-Picture interface members. Opening a native window still requires +//! a presentation backend; these bindings do not manufacture a successful request. + +use super::media_queries; +use crate::{ + util::{get_private_value, set_private_value, v8str}, + web_api_interfaces, +}; +use moli_webapi_declare::WebApiFunctionTemplate; + +const WIDTH: &str = "__moliPictureInPictureWindowWidth"; +const HEIGHT: &str = "__moliPictureInPictureWindowHeight"; +const ON_RESIZE: &str = "__moliPictureInPictureWindowOnresize"; +const LISTENERS: &str = "__moliPictureInPictureWindowListeners"; + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::PictureInPictureWindow, enumerable, receiver)] +struct PictureInPictureWindowPrototype { + #[webapi(accessor_property, getter = dimension_getter, data = v8str(scope, WIDTH))] + width: (), + #[webapi(accessor_property, getter = dimension_getter, data = v8str(scope, HEIGHT))] + height: (), + #[webapi(accessor_property, getter = resize_handler_getter, setter = resize_handler_setter)] + onresize: (), +} + +pub(super) fn install<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, + name: &str, +) { + if name == "PictureInPictureWindow" { + PictureInPictureWindowPrototype::initialize_prototype_template( + scope, + template.prototype_template(scope), + ); + } +} + +fn target<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, +) -> v8::Local<'s, v8::Object> { + moli_webapi_declare::web_api_object_target(scope, receiver) + .expect("PictureInPictureWindow receiver was validated") +} + +fn dimension_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let window = target(scope, args.this()); + let slot = args.data().to_rust_string_lossy(scope); + // Closed windows have zero dimensions. No presentation backend currently + // produces an opened PictureInPictureWindow. + rv.set( + get_private_value(scope, window, &slot) + .unwrap_or_else(|| v8::Integer::new(scope, 0).into()), + ); +} + +fn resize_handler_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let window = target(scope, args.this()); + rv.set(get_private_value(scope, window, ON_RESIZE).unwrap_or_else(|| v8::null(scope).into())); +} + +fn resize_handler_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let window = target(scope, args.this()); + let active = args.get(0).is_object(); + let handler = if active { + args.get(0) + } else { + v8::null(scope).into() + }; + set_private_value(scope, window, ON_RESIZE, handler); + media_queries::simple_object_event_set_ordered_handler( + scope, window, LISTENERS, "resize", ON_RESIZE, active, + ); +} + +/// A closed native window for binding tests, never a production PiP request. +#[cfg(test)] +pub(crate) fn closed_window_for_test<'s>( + scope: &mut v8::PinScope<'s, '_>, +) -> v8::Local<'s, v8::Object> { + #[derive(Default, moli_webapi_declare::WebApiObject)] + #[webapi(interface = web_api_interfaces::PictureInPictureWindow)] + struct ClosedWindow { + #[webapi(slot = super::shared::SIMPLE_EVENT_TARGET_SLOT, value = LISTENERS)] + event_target: (), + #[webapi(slot = super::shared::SIMPLE_EVENT_TARGET_ORDERED_HANDLERS_SLOT, init = true)] + ordered_handlers: (), + } + ClosedWindow::default() + .bind(scope) + .expect("closed native PiP window") +} diff --git a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs index 4c7c3354fc..569ef1c8cc 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs @@ -4,6 +4,14 @@ use crate::web_api_interfaces; use std::collections::HashSet; const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[ + ConstructorSpec { + interface: web_api_interfaces::PictureInPictureEvent::DESCRIPTOR, + kind: ConstructorKind::PictureInPictureEvent, + }, + ConstructorSpec { + interface: web_api_interfaces::PictureInPictureWindow::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, ConstructorSpec { interface: web_api_interfaces::AuthenticatorAssertionResponse::DESCRIPTOR, kind: ConstructorKind::Illegal, diff --git a/moli-renderer-v8/src/context_bootstrap/specs/types.rs b/moli-renderer-v8/src/context_bootstrap/specs/types.rs index d972eda254..81aa49b066 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/types.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/types.rs @@ -14,6 +14,7 @@ pub(in crate::context_bootstrap) enum ConstructorKind { ToolActivatedEvent, ToolCancelEvent, GamepadEvent, + PictureInPictureEvent, UiEvent, FocusEvent, CompositionEvent, diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 06891f0172..9f893058d1 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -1,5 +1,6 @@ mod credential_container; mod credential_json_options; +mod picture_in_picture_interfaces; mod writable_stream_native_receivers; use super::post_parse::dynamic_script_execute_is_runnable_before_dom_content_loaded; use super::{ diff --git a/moli-renderer-v8/src/script_vm/tests/picture_in_picture_interfaces.js b/moli-renderer-v8/src/script_vm/tests/picture_in_picture_interfaces.js new file mode 100644 index 0000000000..afaed7609f --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/picture_in_picture_interfaces.js @@ -0,0 +1,99 @@ +(() => { + const checks = []; + function check(name, action) { + try { checks.push({name, passed: action() === true}); } + catch (error) { checks.push({name, passed: false, error: String(error)}); } + } + const caught = action => { try { action(); return null; } catch (error) { return error; } }; + const frame = document.createElement('iframe'); + document.body.appendChild(frame); + // Standalone lightweight popup shells do not provide a full Window realm. + // Exercise the root and child realms that install native interface templates. + const owners = [window, frame.contentWindow]; + try { + for (const [realm, owner] of owners.entries()) { + const Window = owner.PictureInPictureWindow; + const Event = owner.PictureInPictureEvent; + const typeError = error => error !== null && Object.getPrototypeOf(error) === owner.TypeError.prototype; + check(`${realm}/Window constructor and inheritance`, () => Window.name === 'PictureInPictureWindow' && Window.length === 0 && + Object.getPrototypeOf(Window.prototype) === owner.EventTarget.prototype && + Object.getOwnPropertyDescriptor(Window.prototype, Symbol.toStringTag).value === 'PictureInPictureWindow'); + check(`${realm}/event constructor and inheritance`, () => Event.name === 'PictureInPictureEvent' && Event.length === 2 && + Object.getPrototypeOf(Event.prototype) === owner.Event.prototype && + Object.getOwnPropertyDescriptor(Event.prototype, Symbol.toStringTag).value === 'PictureInPictureEvent'); + let traps = 0; + const proxy = new Proxy({}, {get() { traps++; throw Error('get trap'); }, getPrototypeOf() { traps++; throw Error('prototype trap'); }}); + const revoked = Proxy.revocable({}, {}); revoked.revoke(); + const invalidReceivers = [undefined, null, {}, new owner.EventTarget(), new owner.Event('x'), Window.prototype, + Object.create(Window.prototype), Object.create(Event.prototype), proxy, revoked.proxy]; + for (const [name, prototype, setter] of [['width', Window.prototype, false], ['height', Window.prototype, false], + ['onresize', Window.prototype, true], ['pictureInPictureWindow', Event.prototype, false]]) { + const descriptor = Object.getOwnPropertyDescriptor(prototype, name); + check(`${realm}/${name}/own enumerable configurable accessor`, () => !!descriptor && descriptor.enumerable && descriptor.configurable && + typeof descriptor.get === 'function' && descriptor.get.length === 0 && + (setter ? typeof descriptor.set === 'function' && descriptor.set.length === 1 : descriptor.set === undefined)); + for (const [index, receiver] of invalidReceivers.entries()) { + check(`${realm}/${name}/invalid getter receiver ${index}`, () => typeError(caught(() => descriptor.get.call(receiver)))); + if (setter) { + check(`${realm}/${name}/invalid setter receiver ${index}`, () => { + let conversions = 0; + const value = {toString() { conversions++; throw Error('unexpected conversion'); }}; + return typeError(caught(() => descriptor.set.call(receiver, value))) && conversions === 0; + }); + } + } + } + for (const construct of [false, true]) { + check(`${realm}/Window illegal constructor/${construct}`, () => { + let conversions = 0; + const value = {toString() { conversions++; throw Error('unexpected conversion'); }}; + const error = caught(() => construct ? new Window(value) : Window(value)); + return typeError(error) && conversions === 0; + }); + } + for (const count of [0, 1]) { + check(`${realm}/event requires two arguments before type conversion/${count}`, () => { + let conversions = 0; + const value = {toString() { conversions++; throw Error('unexpected conversion'); }}; + return typeError(caught(() => Reflect.construct(Event, count ? [value] : []))) && conversions === 0; + }); + } + check(`${realm}/event requires new before argument conversion`, () => { + let conversions = 0; + return typeError(caught(() => Event({toString() { conversions++; throw Error('unexpected conversion'); }}, {}))) && conversions === 0; + }); + for (const [index, dictionary] of [undefined, null, {}, false, 1, 'x', 1n, Symbol('dictionary')].entries()) { + check(`${realm}/invalid or incomplete dictionary/${index}`, () => typeError(caught(() => new Event('x', dictionary)))); + } + for (const [index, value] of [undefined, null, false, 1, 'x', 1n, Symbol('window'), {}, Window.prototype, + Object.create(Window.prototype), Object.create(new owner.EventTarget()), proxy, revoked.proxy].entries()) { + check(`${realm}/required native window brand/${index}`, () => typeError(caught(() => new Event('x', {pictureInPictureWindow: value})))); + } + for (const throwingMember of ['type', 'bubbles', 'cancelable', 'composed', 'pictureInPictureWindow', 'none']) { + check(`${realm}/dictionary conversion order and exception identity/${throwingMember}`, () => { + const log = [], marker = {}; + const read = name => { + log.push(name); + if (name === throwingMember) throw marker; + return name === 'type' ? 'resize' : name === 'pictureInPictureWindow' ? {} : false; + }; + const type = {toString() { return read('type'); }}; + const dictionary = {}; + for (const name of ['bubbles', 'cancelable', 'composed', 'pictureInPictureWindow']) { + Object.defineProperty(dictionary, name, {get() { return read(name); }}); + } + const error = caught(() => new Event(type, dictionary)); + const names = ['type', 'bubbles', 'cancelable', 'composed', 'pictureInPictureWindow']; + const expected = throwingMember === 'none' ? names : names.slice(0, names.indexOf(throwingMember) + 1); + return JSON.stringify(log) === JSON.stringify(expected) && (throwingMember === 'none' ? typeError(error) : error === marker); + }); + } + check(`${realm}/author and revoked Proxy brand checks do not invoke traps`, () => traps === 0); + } + } finally { + frame.remove(); + } + const facts = {complete: true, total: checks.length, passed: checks.filter(row => row.passed).length, checks}; + globalThis.__uiEventResults = facts; + return facts.passed === facts.total; +})() diff --git a/moli-renderer-v8/src/script_vm/tests/picture_in_picture_interfaces.rs b/moli-renderer-v8/src/script_vm/tests/picture_in_picture_interfaces.rs new file mode 100644 index 0000000000..0ee0dfbee8 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/picture_in_picture_interfaces.rs @@ -0,0 +1,134 @@ +use super::*; + +#[test] +fn picture_in_picture_interfaces_validate_descriptors_receivers_and_required_dictionary_members() { + let mut vm = new_storage_page_task_executor_test_vm("https://picture-in-picture.test/"); + assert_eq!( + vm.eval(include_str!("picture_in_picture_interfaces.js")) + .unwrap(), + "true", + "{}", + vm.eval("JSON.stringify(__uiEventResults.checks.filter(row => !row.passed))") + .unwrap() + ); +} + +fn install_closed_window(vm: &mut ScriptVm) { + let context_ptr = &vm.page_default_runtime.context as *const _; + vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| { + let global = scope.get_current_context().global(scope); + let window = crate::context_bootstrap::closed_picture_in_picture_window_for_test(scope); + let handler = crate::util::new_null_prototype_object(scope); + let proxy = v8::Proxy::new(scope, window, handler).unwrap(); + moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap(); + for (name, value) in [ + ("nativeWindow", window.into()), + ("nativeProxy", proxy.into()), + ] { + assert_eq!( + global.create_data_property(scope, crate::util::v8str(scope, name).into(), value), + Some(true) + ); + } + Ok(()) + }) + .unwrap(); +} + +#[test] +fn picture_in_picture_events_retain_native_window_identity_across_realms_and_author_mutations() { + let mut vm = new_storage_page_task_executor_test_vm("https://picture-in-picture-events.test/"); + vm.eval("document.body.innerHTML = ''") + .unwrap(); + install_closed_window(&mut vm); + assert_eq!(vm.eval(r#"(() => { + const assert = (ok, name) => {if (!ok) throw Error(name)}; + const other = document.querySelector('iframe').contentWindow; + const payload = Object.getOwnPropertyDescriptor(other.PictureInPictureEvent.prototype, 'pictureInPictureWindow').get; + const width = Object.getOwnPropertyDescriptor(other.PictureInPictureWindow.prototype, 'width').get; + assert(nativeWindow.width === 0 && nativeWindow.height === 0 && nativeProxy.width === 0, 'closed dimensions'); + for (const window of [nativeWindow, nativeProxy]) { + const event = new other.PictureInPictureEvent('enterpictureinpicture', {pictureInPictureWindow: window, bubbles:true, cancelable:true, composed:true}); + assert(event instanceof other.PictureInPictureEvent && event instanceof other.Event && !(event instanceof PictureInPictureEvent), 'constructor realm'); + assert(event.pictureInPictureWindow === window && event.pictureInPictureWindow === event.pictureInPictureWindow, 'SameObject preserves original argument'); + assert(event.type === 'enterpictureinpicture' && event.bubbles && event.cancelable && event.composed && !event.isTrusted, 'inherited EventInit'); + Object.defineProperty(event, 'pictureInPictureWindow', {value: {}, configurable:true}); + assert(payload.call(event) === window, 'author payload expando does not alter native state'); + const proxy = Proxy.revocable(window, {}); proxy.revoke(); + let traps = 0; + const author = new Proxy(window, {get() {traps++; throw Error('get')}, getPrototypeOf() {traps++; throw Error('prototype')}}); + for (const bad of [Object.create(window), author, proxy.proxy]) { + let error; try {new other.PictureInPictureEvent('x', {pictureInPictureWindow:bad})} catch (caught) {error=caught} + assert(Object.getPrototypeOf(error) === other.TypeError.prototype, 'author wrapper rejected in callee realm'); + } + assert(traps === 0, 'native brand does not invoke author traps'); + } + class Derived extends other.PictureInPictureEvent {} + const derived = new Derived('x', {pictureInPictureWindow:nativeWindow}); + assert(derived instanceof Derived && payload.call(derived) === nativeWindow, 'subclass native brand'); + Object.defineProperty(nativeWindow, 'width', {get() {throw Error('public dimension read')}}); + Object.setPrototypeOf(nativeWindow, null); + assert(width.call(nativeWindow) === 0 && payload.call(derived) === nativeWindow, 'private identity survives prototype and expando changes'); + return true; + })()"#).unwrap(), "true"); +} + +#[test] +fn picture_in_picture_resize_handlers_share_listener_order_and_cancellation_state() { + let mut vm = new_storage_page_task_executor_test_vm("https://picture-in-picture-resize.test/"); + install_closed_window(&mut vm); + assert_eq!(vm.eval(r#"(() => { + const assert = (ok, name) => {if (!ok) throw Error(name)}; + const target = nativeWindow, order = []; + const descriptor = Object.getOwnPropertyDescriptor(PictureInPictureWindow.prototype, 'onresize'); + assert(target.onresize === null, 'initial handler'); + target.addEventListener('resize', event => {order.push('before'); assert(event.target === target, 'native target')}); + target.onresize = function(event) {order.push('original'); assert(this === target && event.currentTarget === target, 'callback this and currentTarget')}; + target.addEventListener('resize', () => order.push('after')); + nativeProxy.onresize = function(event) {order.push('replacement'); assert(this === target, 'native proxy updates same handler'); return false}; + const event = new Event('resize', {cancelable:true}); + assert(target.dispatchEvent(event) === false && event.defaultPrevented, 'handler cancellation'); + assert(order.join() === 'before,replacement,after' && target.onresize === nativeProxy.onresize, 'replacement preserves listener position'); + for (const value of [undefined, null, false, 3, 'x', 1n, Symbol('handler')]) { + descriptor.set.call(target, value); + assert(target.onresize === null, 'primitive handler becomes null'); + } + order.length = 0; + descriptor.set.call(nativeProxy, () => order.push('last')); + assert(target.dispatchEvent(new Event('resize')) && order.join() === 'before,after,last', 'cleared handler is appended when set again'); + assert(event.currentTarget === null && target.width === 0 && target.height === 0, 'dispatch does not open a window'); + return true; + })()"#).unwrap(), "true"); +} + +#[test] +fn picture_in_picture_native_event_proxies_read_the_original_private_payload() { + let mut vm = + new_storage_page_task_executor_test_vm("https://picture-in-picture-event-proxy.test/"); + install_closed_window(&mut vm); + vm.eval("globalThis.nativeEvent = new PictureInPictureEvent('enterpictureinpicture', {pictureInPictureWindow:nativeWindow})").unwrap(); + let context_ptr = &vm.page_default_runtime.context as *const _; + vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| { + let global = scope.get_current_context().global(scope); + let key = crate::util::v8str(scope, "nativeEvent"); + let event = + v8::Local::::try_from(global.get(scope, key.into()).unwrap()).unwrap(); + let handler = crate::util::new_null_prototype_object(scope); + let proxy = v8::Proxy::new(scope, event, handler).unwrap(); + moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap(); + let key = crate::util::v8str(scope, "eventProxy"); + assert_eq!( + global.create_data_property(scope, key.into(), proxy.into()), + Some(true) + ); + Ok(()) + }) + .unwrap(); + assert_eq!(vm.eval(r#"(() => { + const get = Object.getOwnPropertyDescriptor(PictureInPictureEvent.prototype, 'pictureInPictureWindow').get; + if (get.call(eventProxy) !== nativeWindow) throw Error('native event Proxy payload'); + let error; try {get.call(new Proxy(eventProxy, {}))} catch(caught) {error=caught} + if (!(error instanceof TypeError)) throw Error('author event Proxy accepted'); + return true; + })()"#).unwrap(), "true"); +} diff --git a/moli-renderer-v8/src/web_api_interfaces.rs b/moli-renderer-v8/src/web_api_interfaces.rs index d8245aeacc..d04e5b2528 100644 --- a/moli-renderer-v8/src/web_api_interfaces.rs +++ b/moli-renderer-v8/src/web_api_interfaces.rs @@ -192,6 +192,8 @@ interfaces! { Gamepad; GamepadButton; GamepadEvent: Event; + PictureInPictureEvent: Event; + PictureInPictureWindow: EventTarget; GamepadHapticActuator; HTMLAllCollection; HTMLAnchorElement: HTMLElement;