mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
feat(webrtc): implement ICE and data channel event constructors
Expose RTCPeerConnectionIceEvent and RTCDataChannelEvent with native Event inheritance, dictionary conversion, branded payload validation and readonly attributes. Tests cover conversion order, exceptions, cross-realm values, subclassing, reinitialization and Window-only exposure. Validation: all branch checks passed. Summary [ 104.941s] 18927 tests run: 18927 passed (9 slow), 16 skipped
This commit is contained in:
@@ -61,7 +61,9 @@ use super::super::{
|
||||
offline_audio_context_constructor_callback,
|
||||
},
|
||||
webrtc::{
|
||||
rtc_ice_candidate_constructor_callback, rtc_peer_connection_constructor_callback,
|
||||
rtc_data_channel_event_constructor_callback, rtc_ice_candidate_constructor_callback,
|
||||
rtc_peer_connection_constructor_callback,
|
||||
rtc_peer_connection_ice_event_constructor_callback,
|
||||
rtc_session_description_constructor_callback,
|
||||
},
|
||||
websocket::{
|
||||
@@ -600,6 +602,16 @@ pub(in crate::context_bootstrap) fn build_constructor_template<'s>(
|
||||
.length(1)
|
||||
.build(scope)
|
||||
}
|
||||
ConstructorKind::RtcPeerConnectionIceEvent => {
|
||||
v8::FunctionTemplate::builder(rtc_peer_connection_ice_event_constructor_callback)
|
||||
.length(1)
|
||||
.build(scope)
|
||||
}
|
||||
ConstructorKind::RtcDataChannelEvent => {
|
||||
v8::FunctionTemplate::builder(rtc_data_channel_event_constructor_callback)
|
||||
.length(2)
|
||||
.build(scope)
|
||||
}
|
||||
ConstructorKind::Navigator
|
||||
| ConstructorKind::WorkerNavigator
|
||||
| ConstructorKind::Permissions
|
||||
|
||||
@@ -349,6 +349,7 @@ pub(super) use base::{
|
||||
event_target_getter_function, event_type_getter_function, set_event_dispatch_fields,
|
||||
set_event_initialized,
|
||||
};
|
||||
pub(in crate::context_bootstrap) use init::parse_event_init;
|
||||
pub(super) use kind::EventSubclassKind;
|
||||
pub(super) use methods::{
|
||||
event_cancel_bubble_getter_function, event_cancel_bubble_setter_function,
|
||||
|
||||
@@ -94,10 +94,16 @@ pub(super) fn read_event_init<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
) -> (bool, bool, bool) {
|
||||
let Some(init) = webidl::optional_object_arg(args, 1) else {
|
||||
return (false, false, false);
|
||||
parse_event_init(scope, webidl::optional_object_arg(args, 1)).unwrap_or((false, false, false))
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn parse_event_init<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
init: Option<v8::Local<'s, v8::Object>>,
|
||||
) -> Result<(bool, bool, bool), webidl::WebIdlError> {
|
||||
let Some(init) = init else {
|
||||
return Ok((false, false, false));
|
||||
};
|
||||
webidl::parse_dictionary_object::<EventInitMembers>(scope, init)
|
||||
.map(|parsed| (parsed.bubbles, parsed.cancelable, parsed.composed))
|
||||
.unwrap_or((false, false, false))
|
||||
}
|
||||
|
||||
@@ -1933,6 +1933,8 @@ pub(crate) fn finish_context_bootstrap(
|
||||
("RTCPeerConnection", "RTCPeerConnection"),
|
||||
("RTCIceCandidate", "RTCIceCandidate"),
|
||||
("RTCSessionDescription", "RTCSessionDescription"),
|
||||
("RTCPeerConnectionIceEvent", "RTCPeerConnectionIceEvent"),
|
||||
("RTCDataChannelEvent", "RTCDataChannelEvent"),
|
||||
("RTCRtpReceiver", "RTCRtpReceiver"),
|
||||
("RTCDataChannel", "RTCDataChannel"),
|
||||
("Blob", "Blob"),
|
||||
|
||||
@@ -867,6 +867,14 @@ const CONSTRUCTOR_SPECS_AFTER_STREAMS: &[ConstructorSpec] = &[
|
||||
interface: web_api_interfaces::RTCSessionDescription::DESCRIPTOR,
|
||||
kind: ConstructorKind::RtcSessionDescription,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::RTCPeerConnectionIceEvent::DESCRIPTOR,
|
||||
kind: ConstructorKind::RtcPeerConnectionIceEvent,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::RTCDataChannelEvent::DESCRIPTOR,
|
||||
kind: ConstructorKind::RtcDataChannelEvent,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::RTCRtpReceiver::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
|
||||
@@ -90,6 +90,8 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
|
||||
RtcPeerConnection,
|
||||
RtcIceCandidate,
|
||||
RtcSessionDescription,
|
||||
RtcPeerConnectionIceEvent,
|
||||
RtcDataChannelEvent,
|
||||
Navigator,
|
||||
WorkerNavigator,
|
||||
Permissions,
|
||||
|
||||
@@ -5,9 +5,13 @@ use crate::util::{
|
||||
use crate::web_api_interfaces;
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
|
||||
mod events;
|
||||
mod ice_candidate;
|
||||
mod ice_candidate_parser;
|
||||
mod session_description;
|
||||
pub(in crate::context_bootstrap) use events::{
|
||||
rtc_data_channel_event_constructor_callback, rtc_peer_connection_ice_event_constructor_callback,
|
||||
};
|
||||
pub(in crate::context_bootstrap) use ice_candidate::rtc_ice_candidate_constructor_callback;
|
||||
pub(in crate::context_bootstrap) use session_description::rtc_session_description_constructor_callback;
|
||||
|
||||
@@ -203,6 +207,9 @@ pub(in crate::context_bootstrap) fn install_webrtc_template_bindings<'s>(
|
||||
) {
|
||||
let prototype = template.prototype_template(scope);
|
||||
match interface_name {
|
||||
"RTCPeerConnectionIceEvent" | "RTCDataChannelEvent" => {
|
||||
events::install_event_template_bindings(scope, template, interface_name)
|
||||
}
|
||||
"RTCIceCandidate" => {
|
||||
ice_candidate::install_ice_candidate_template_bindings(scope, template)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
use super::{ice_candidate::ice_candidate_receiver_branded, rtc_data_channel_receiver_branded};
|
||||
use crate::web_api_interfaces;
|
||||
use crate::{
|
||||
context_bootstrap::events::{initialize_event_object_with_type, parse_event_init},
|
||||
util::{
|
||||
apply_webidl_constructor_prototype_fallback, callback_data_index_value, callback_data_item,
|
||||
get_private_value, throw_type_error, v8str,
|
||||
},
|
||||
webidl::{self, WebIdlConverter},
|
||||
};
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
|
||||
const ICE_EVENT_CANDIDATE_SLOT: &str = "__moliRtcIceEventCandidate";
|
||||
const ICE_EVENT_URL_SLOT: &str = "__moliRtcIceEventUrl";
|
||||
const DATA_CHANNEL_EVENT_CHANNEL_SLOT: &str = "__moliRtcDataChannelEventChannel";
|
||||
const EVENT_MEMBER_SLOTS: &[&str] = &[
|
||||
ICE_EVENT_CANDIDATE_SLOT,
|
||||
ICE_EVENT_URL_SLOT,
|
||||
DATA_CHANNEL_EVENT_CHANNEL_SLOT,
|
||||
];
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = web_api_interfaces::RTCPeerConnectionIceEvent)]
|
||||
struct IceEventObjectDeclaration<'scope> {
|
||||
#[webapi(data_property, enumerable)]
|
||||
composed: bool,
|
||||
#[webapi(slot = ICE_EVENT_CANDIDATE_SLOT)]
|
||||
candidate: v8::Local<'scope, v8::Value>,
|
||||
#[webapi(slot = ICE_EVENT_URL_SLOT)]
|
||||
url: v8::Local<'scope, v8::Value>,
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = web_api_interfaces::RTCDataChannelEvent)]
|
||||
struct DataChannelEventObjectDeclaration<'scope> {
|
||||
#[webapi(data_property, enumerable)]
|
||||
composed: bool,
|
||||
#[webapi(slot = DATA_CHANNEL_EVENT_CHANNEL_SLOT)]
|
||||
channel: v8::Local<'scope, v8::Value>,
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::RTCPeerConnectionIceEvent, receiver, enumerable)]
|
||||
struct IceEventPrototypeDeclaration {
|
||||
#[webapi(accessor_property, getter = event_member_getter, data = callback_data_index_value(scope, 0))]
|
||||
candidate: (),
|
||||
#[webapi(accessor_property, getter = event_member_getter, data = callback_data_index_value(scope, 1))]
|
||||
url: (),
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::RTCDataChannelEvent, receiver, enumerable)]
|
||||
struct DataChannelEventPrototypeDeclaration {
|
||||
#[webapi(accessor_property, getter = event_member_getter, data = callback_data_index_value(scope, 2))]
|
||||
channel: (),
|
||||
}
|
||||
|
||||
pub(super) fn install_event_template_bindings<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_, ()>,
|
||||
template: v8::Local<'s, v8::FunctionTemplate>,
|
||||
interface_name: &str,
|
||||
) {
|
||||
let prototype = template.prototype_template(scope);
|
||||
match interface_name {
|
||||
"RTCPeerConnectionIceEvent" => {
|
||||
IceEventPrototypeDeclaration::initialize_prototype_template(scope, prototype)
|
||||
}
|
||||
"RTCDataChannelEvent" => {
|
||||
DataChannelEventPrototypeDeclaration::initialize_prototype_template(scope, prototype)
|
||||
}
|
||||
_ => unreachable!("unsupported WebRTC event interface"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn rtc_peer_connection_ice_event_constructor_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
construct_event(scope, args, rv, false);
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn rtc_data_channel_event_constructor_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
construct_event(scope, args, rv, true);
|
||||
}
|
||||
|
||||
fn construct_event<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
is_data_channel_event: bool,
|
||||
) {
|
||||
let (name, required_arguments, member) = if is_data_channel_event {
|
||||
("RTCDataChannelEvent", 2, "channel")
|
||||
} else {
|
||||
("RTCPeerConnectionIceEvent", 1, "candidate")
|
||||
};
|
||||
if !args.is_construct_call() || args.length() < required_arguments {
|
||||
throw_type_error(
|
||||
scope,
|
||||
&format!("{name} requires 'new' and {required_arguments} argument(s)."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if args.get(0).is_symbol() {
|
||||
throw_type_error(scope, "Cannot convert a Symbol to a DOMString.");
|
||||
return;
|
||||
}
|
||||
// Retain the DOMString in V8 instead of replacing lone UTF-16 surrogates.
|
||||
let Some(event_type) = args.get(0).to_string(scope) else {
|
||||
return;
|
||||
};
|
||||
let init = match webidl::dictionary_arg(&args, 1, webidl::Context::argument(name, 2)) {
|
||||
Ok(init) => init,
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Convert inherited EventInit members before the subclass's own members.
|
||||
let (bubbles, cancelable, composed) = match parse_event_init(scope, init) {
|
||||
Ok(flags) => flags,
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let value = if let Some(init) = init {
|
||||
let Some(value) = init.get(scope, v8str(scope, member).into()) else {
|
||||
return;
|
||||
};
|
||||
value
|
||||
} else {
|
||||
v8::undefined(scope).into()
|
||||
};
|
||||
let value = if !is_data_channel_event && value.is_null_or_undefined() {
|
||||
v8::null(scope).into()
|
||||
} else {
|
||||
let branded = v8::Local::<v8::Object>::try_from(value)
|
||||
.ok()
|
||||
.is_some_and(|object| {
|
||||
if is_data_channel_event {
|
||||
rtc_data_channel_receiver_branded(scope, object)
|
||||
} else {
|
||||
ice_candidate_receiver_branded(scope, object)
|
||||
}
|
||||
});
|
||||
if !branded {
|
||||
throw_type_error(
|
||||
scope,
|
||||
&format!(
|
||||
"{name}.{member} must be a genuine {} object.",
|
||||
if is_data_channel_event {
|
||||
"RTCDataChannel"
|
||||
} else {
|
||||
"RTCIceCandidate"
|
||||
}
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
value
|
||||
};
|
||||
let mut url = v8::null(scope).into();
|
||||
if !is_data_channel_event && let Some(init) = init {
|
||||
let Some(raw) = init.get(scope, v8str(scope, "url").into()) else {
|
||||
return;
|
||||
};
|
||||
if !raw.is_null_or_undefined() {
|
||||
let converted = match webidl::UsvString::convert(
|
||||
scope,
|
||||
raw,
|
||||
webidl::Context::member("RTCPeerConnectionIceEventInit", "url"),
|
||||
&Default::default(),
|
||||
) {
|
||||
Ok(value) => value.0,
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(converted) = v8::String::new(scope, &converted) else {
|
||||
return;
|
||||
};
|
||||
url = converted.into();
|
||||
}
|
||||
}
|
||||
let event = args.this();
|
||||
initialize_event_object_with_type(scope, event, event_type, bubbles, cancelable);
|
||||
let initialized = if is_data_channel_event {
|
||||
DataChannelEventObjectDeclaration::new(composed, value).initialize(scope, event)
|
||||
} else {
|
||||
IceEventObjectDeclaration::new(composed, value, url).initialize(scope, event)
|
||||
};
|
||||
if initialized.is_err() {
|
||||
return;
|
||||
}
|
||||
apply_webidl_constructor_prototype_fallback(scope, event, args.new_target(), name);
|
||||
rv.set(event.into());
|
||||
}
|
||||
|
||||
fn event_member_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(slot) = callback_data_item(scope, &args, EVENT_MEMBER_SLOTS, "WebRTC event attribute")
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if let Some(value) = get_private_value(scope, args.this(), slot) {
|
||||
rv.set(value);
|
||||
} else {
|
||||
throw_type_error(scope, "Illegal invocation");
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,13 @@ fn parse_candidate_init<'s>(
|
||||
Some(values)
|
||||
}
|
||||
|
||||
pub(super) fn ice_candidate_receiver_branded<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
web_api_interfaces::RTCIceCandidate::is_instance(scope, receiver)
|
||||
}
|
||||
|
||||
fn candidate_values<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -53,6 +53,7 @@ mod trusted_types;
|
||||
mod value_events;
|
||||
mod web_audio;
|
||||
mod webrtc;
|
||||
mod webrtc_events;
|
||||
mod window_legacy_objects;
|
||||
mod worker_listener_invocation;
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn webrtc_ice_event_constructor_preserves_candidates_and_converts_nullable_urls() {
|
||||
let mut vm = new_storage_test_vm("http://webrtc-ice-event.test/");
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const rejects = fn => {
|
||||
try { fn(); return false; } catch (error) { return error instanceof TypeError; }
|
||||
};
|
||||
const candidate = new RTCIceCandidate({sdpMid: 'audio'});
|
||||
const rawType = 'ice\ud800\0\udc00';
|
||||
const event = new RTCPeerConnectionIceEvent(rawType, {candidate, url: 'turn:\ud800'});
|
||||
const checks = [RTCPeerConnectionIceEvent.length === 1,
|
||||
rejects(() => new RTCPeerConnectionIceEvent()),
|
||||
rejects(() => RTCPeerConnectionIceEvent('ice')),
|
||||
rejects(() => new RTCPeerConnectionIceEvent(Symbol())),
|
||||
event.type === rawType, event.candidate === candidate, event.url === 'turn:\ufffd',
|
||||
new RTCPeerConnectionIceEvent(undefined).type === 'undefined',
|
||||
new RTCPeerConnectionIceEvent(null).type === 'null'];
|
||||
for (const init of [undefined, null, {}, {candidate: null, url: null},
|
||||
{candidate: undefined, url: undefined}]) {
|
||||
const empty = new RTCPeerConnectionIceEvent('ice', init);
|
||||
checks.push(empty.candidate === null, empty.url === null,
|
||||
!empty.bubbles, !empty.cancelable, !empty.composed);
|
||||
}
|
||||
for (const url of ['', false, 4, 5n, ['not a URL']]) {
|
||||
checks.push(new RTCPeerConnectionIceEvent('ice', {url}).url === String(url));
|
||||
}
|
||||
for (const init of [true, 1, 'init', Symbol(), 1n, {url: Symbol()},
|
||||
{candidate: {}}, {candidate: false}, {candidate: 1},
|
||||
{candidate: Object.create(candidate)}, {candidate: new Proxy(candidate, {})}]) {
|
||||
checks.push(rejects(() => new RTCPeerConnectionIceEvent('ice', init)));
|
||||
}
|
||||
Object.setPrototypeOf(candidate, null);
|
||||
checks.push(new RTCPeerConnectionIceEvent('ice', {candidate}).candidate === candidate);
|
||||
return checks.every(Boolean) || JSON.stringify(checks);
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect(
|
||||
"ICE events should preserve candidate identity and the DOMString/USVString distinction",
|
||||
);
|
||||
assert_eq!(result, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webrtc_data_channel_event_requires_a_genuine_channel_and_checks_arity_first() {
|
||||
let mut vm = new_storage_test_vm("https://webrtc-data-channel-event.test/");
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
const rejects = fn => {
|
||||
try { fn(); return false; } catch (error) { return error instanceof TypeError; }
|
||||
};
|
||||
const peer = new RTCPeerConnection();
|
||||
const channel = peer.createDataChannel('test');
|
||||
const event = new RTCDataChannelEvent('channel', {channel});
|
||||
let converted = false;
|
||||
const checks = [RTCDataChannelEvent.length === 2, event.channel === channel,
|
||||
rejects(() => RTCDataChannelEvent('channel', {channel})),
|
||||
rejects(() => new RTCDataChannelEvent()),
|
||||
rejects(() => new RTCDataChannelEvent({toString() { converted = true; return 'channel'; }})),
|
||||
!converted];
|
||||
for (const init of [undefined, null, {}, {channel: null}, {channel: undefined},
|
||||
false, 1, 'init', Symbol(), 1n, {channel: {}},
|
||||
{channel: Object.create(RTCDataChannel.prototype)},
|
||||
{channel: Object.create(channel)}, {channel: new Proxy(channel, {})},
|
||||
{channel: new RTCIceCandidate({sdpMid: 'audio'})}]) {
|
||||
checks.push(rejects(() => new RTCDataChannelEvent('channel', init)));
|
||||
}
|
||||
Object.defineProperty(RTCDataChannel, Symbol.hasInstance, {
|
||||
configurable: true, value() { throw new Error('must not use instanceof'); }
|
||||
});
|
||||
try {
|
||||
checks.push(rejects(() => new RTCDataChannelEvent('channel', {channel: {}})),
|
||||
new RTCDataChannelEvent('channel', {channel}).channel === channel);
|
||||
} finally { delete RTCDataChannel[Symbol.hasInstance]; }
|
||||
Object.setPrototypeOf(channel, null);
|
||||
checks.push(new RTCDataChannelEvent('channel', {channel}).channel === channel);
|
||||
peer.close();
|
||||
return checks.every(Boolean) || JSON.stringify(checks);
|
||||
})()
|
||||
"#).expect("data channel events should validate arity before conversion and use native interface branding");
|
||||
assert_eq!(result, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webrtc_event_dictionary_conversion_reads_inherited_members_first_and_preserves_exceptions() {
|
||||
let mut vm = new_storage_test_vm("https://webrtc-event-conversion.test/");
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
const candidate = new RTCIceCandidate({sdpMid: 'audio'});
|
||||
const peer = new RTCPeerConnection();
|
||||
const channel = peer.createDataChannel('test');
|
||||
const checks = [];
|
||||
for (const [Ctor, ownMembers] of [
|
||||
[RTCPeerConnectionIceEvent, ['candidate', 'url']], [RTCDataChannelEvent, ['channel']]
|
||||
]) {
|
||||
const names = ['bubbles', 'cancelable', 'composed', ...ownMembers];
|
||||
const reads = [];
|
||||
const dictionary = Object.create({bubbles: [], cancelable: 1, composed: 'yes',
|
||||
candidate, channel, url: 'url'});
|
||||
const event = new Ctor({toString() { reads.push('type'); return 'converted'; }},
|
||||
new Proxy(dictionary, {get(object, name) { reads.push(name); return object[name]; }}));
|
||||
checks.push(reads.join(',') === ['type', ...names].join(','),
|
||||
event.type === 'converted', event.bubbles, event.cancelable, event.composed);
|
||||
const sentinel = new RangeError('original getter failure');
|
||||
for (const name of names) {
|
||||
const bad = {candidate, channel};
|
||||
Object.defineProperty(bad, name, {get() { throw sentinel; }});
|
||||
try { new Ctor('event', bad); checks.push(false); }
|
||||
catch (error) { checks.push(error === sentinel); }
|
||||
}
|
||||
try { new Ctor({toString() { throw sentinel; }}, {candidate, channel}); checks.push(false); }
|
||||
catch (error) { checks.push(error === sentinel); }
|
||||
}
|
||||
const sentinel = new RangeError('URL conversion');
|
||||
try {
|
||||
new RTCPeerConnectionIceEvent('ice', {url: {toString() { throw sentinel; }}});
|
||||
checks.push(false);
|
||||
} catch (error) { checks.push(error === sentinel); }
|
||||
let urlRead = false;
|
||||
try {
|
||||
new RTCPeerConnectionIceEvent('ice', {candidate: {}, get url() { urlRead = true; return ''; }});
|
||||
checks.push(false);
|
||||
} catch (error) { checks.push(error instanceof TypeError, !urlRead); }
|
||||
Object.defineProperty(Object.prototype, 'bubbles', {
|
||||
configurable: true, get() { throw sentinel; }
|
||||
});
|
||||
try {
|
||||
checks.push(new RTCPeerConnectionIceEvent('ice', null).candidate === null);
|
||||
try { new RTCDataChannelEvent('channel', null); checks.push(false); }
|
||||
catch (error) { checks.push(error instanceof TypeError); }
|
||||
} finally { delete Object.prototype.bubbles; }
|
||||
peer.close();
|
||||
return checks.every(Boolean) || JSON.stringify(checks);
|
||||
})()
|
||||
"#).expect("WebRTC event dictionaries should convert EventInit first without swallowing getters or exceptions");
|
||||
assert_eq!(result, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webrtc_event_members_are_readonly_branded_and_survive_dispatch_and_reinitialization() {
|
||||
let mut vm = new_storage_test_vm("https://webrtc-event-dispatch.test/");
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
const candidate = new RTCIceCandidate({sdpMid: 'audio'});
|
||||
const peer = new RTCPeerConnection();
|
||||
const channel = peer.createDataChannel('test');
|
||||
const checks = [];
|
||||
const rejects = fn => {
|
||||
try { fn(); return false; } catch (error) { return error instanceof TypeError; }
|
||||
};
|
||||
for (const [Ctor, members] of [
|
||||
[RTCPeerConnectionIceEvent, {candidate, url: 'turn:server'}], [RTCDataChannelEvent, {channel}]
|
||||
]) {
|
||||
const event = new Ctor('test', {...members, bubbles: true, cancelable: true, composed: true});
|
||||
checks.push(event instanceof Ctor, event instanceof Event,
|
||||
Object.getPrototypeOf(Ctor.prototype) === Event.prototype,
|
||||
Object.prototype.toString.call(event) === '[object ' + Ctor.name + ']',
|
||||
event.isTrusted === false, typeof event.timeStamp === 'number');
|
||||
for (const [name, original] of Object.entries(members)) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Ctor.prototype, name);
|
||||
if (!descriptor) return 'missing:' + name;
|
||||
checks.push(!Object.hasOwn(event, name), descriptor.enumerable, descriptor.configurable,
|
||||
descriptor.get.name === 'get ' + name, descriptor.get.length === 0, !descriptor.set);
|
||||
event[name] = 'changed';
|
||||
checks.push(event[name] === original, rejects(() => { 'use strict'; event[name] = null; }));
|
||||
for (const fake of [undefined, null, {}, Ctor.prototype, new Event('test'),
|
||||
Object.create(event), new Proxy(event, {})]) {
|
||||
checks.push(rejects(() => descriptor.get.call(fake)));
|
||||
}
|
||||
}
|
||||
const target = new EventTarget();
|
||||
let delivered = false;
|
||||
target.addEventListener('test', received => {
|
||||
delivered = received === event && received.target === target && received.currentTarget === target;
|
||||
received.preventDefault();
|
||||
});
|
||||
checks.push(target.dispatchEvent(event) === false, delivered, event.defaultPrevented,
|
||||
event.isTrusted === false, event.currentTarget === null);
|
||||
event.initEvent('again', false, false);
|
||||
checks.push(event.type === 'again', !event.defaultPrevented, !event.bubbles, !event.cancelable);
|
||||
for (const [name, original] of Object.entries(members)) checks.push(event[name] === original);
|
||||
}
|
||||
peer.close();
|
||||
return checks.every(Boolean) || JSON.stringify(checks);
|
||||
})()
|
||||
"#).expect("WebRTC event attributes should retain native identity through normal Event operations");
|
||||
assert_eq!(result, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webrtc_event_constructors_support_subclasses_foreign_members_and_new_target_realms() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://webrtc-event-realms.test/",
|
||||
"<!doctype html><iframe></iframe>",
|
||||
);
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
const child = document.querySelector('iframe').contentWindow;
|
||||
const foreignPeer = new child.RTCPeerConnection();
|
||||
const candidate = new child.RTCIceCandidate({sdpMid: 'foreign'});
|
||||
const channel = foreignPeer.createDataChannel('foreign');
|
||||
const checks = [];
|
||||
for (const [name, member, value] of [
|
||||
['RTCPeerConnectionIceEvent', 'candidate', candidate], ['RTCDataChannelEvent', 'channel', channel]
|
||||
]) {
|
||||
const Ctor = window[name];
|
||||
const init = {[member]: value};
|
||||
class Derived extends Ctor {}
|
||||
const derived = new Derived('event', init);
|
||||
const foreign = new child[name]('event', init);
|
||||
const newTarget = child.Function('');
|
||||
newTarget.prototype = 0;
|
||||
let prototypeReads = 0;
|
||||
const target = new Proxy(newTarget, {get(object, name) {
|
||||
if (name === 'prototype') prototypeReads++;
|
||||
return Reflect.get(object, name);
|
||||
}});
|
||||
const fallback = Reflect.construct(Ctor, ['event', init], target);
|
||||
checks.push(derived instanceof Derived, derived instanceof Ctor,
|
||||
derived[member] === value, foreign[member] === value,
|
||||
Object.getOwnPropertyDescriptor(Ctor.prototype, member).get.call(foreign) === value,
|
||||
Object.getPrototypeOf(fallback) === child[name].prototype, fallback[member] === value,
|
||||
prototypeReads === 1);
|
||||
}
|
||||
foreignPeer.close();
|
||||
return checks.every(Boolean) || JSON.stringify(checks);
|
||||
})()
|
||||
"#).expect("WebRTC event constructors should preserve genuine cross-realm members and NewTarget semantics");
|
||||
assert_eq!(result, "true");
|
||||
}
|
||||
@@ -353,6 +353,8 @@ interfaces! {
|
||||
RTCDataChannel: EventTarget;
|
||||
RTCIceCandidate;
|
||||
RTCPeerConnection: EventTarget;
|
||||
RTCPeerConnectionIceEvent: Event;
|
||||
RTCDataChannelEvent: Event;
|
||||
RTCRtpReceiver;
|
||||
RTCSessionDescription;
|
||||
RadioNodeList: NodeList;
|
||||
|
||||
@@ -71,6 +71,21 @@ async fn worker_console_capture_does_not_serialize_or_coerce_page_objects() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_does_not_expose_window_only_webrtc_event_interfaces() {
|
||||
ensure_v8();
|
||||
let mut handle = spawn_worker(
|
||||
r#"postMessage([typeof RTCPeerConnectionIceEvent, typeof RTCDataChannelEvent]); close();"#
|
||||
.into(),
|
||||
"test://webrtc-event-exposure".into(),
|
||||
);
|
||||
let msg = timeout(TIMEOUT, handle.recv())
|
||||
.await
|
||||
.expect("timed out waiting for worker WebRTC event exposure")
|
||||
.expect("channel closed");
|
||||
assert_eq!(expect_post_json(msg), r#"["undefined","undefined"]"#);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_postmessage_to_parent() {
|
||||
ensure_v8();
|
||||
|
||||
Reference in New Issue
Block a user