mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 08:07:38 +00:00
feat(media): bind native PictureInPicture event payloads
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<f64>,
|
||||
}
|
||||
|
||||
#[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::<PictureInPictureEventInit>(
|
||||
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::<GamepadEventInit>(scope, dictionary)?;
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -14,6 +14,7 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
|
||||
ToolActivatedEvent,
|
||||
ToolCancelEvent,
|
||||
GamepadEvent,
|
||||
PictureInPictureEvent,
|
||||
UiEvent,
|
||||
FocusEvent,
|
||||
CompositionEvent,
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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;
|
||||
})()
|
||||
@@ -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 = '<iframe></iframe>'")
|
||||
.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::<v8::Object>::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");
|
||||
}
|
||||
@@ -192,6 +192,8 @@ interfaces! {
|
||||
Gamepad;
|
||||
GamepadButton;
|
||||
GamepadEvent: Event;
|
||||
PictureInPictureEvent: Event;
|
||||
PictureInPictureWindow: EventTarget;
|
||||
GamepadHapticActuator;
|
||||
HTMLAllCollection;
|
||||
HTMLAnchorElement: HTMLElement;
|
||||
|
||||
Reference in New Issue
Block a user