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:
ldm0
2026-09-29 09:10:53 +08:00
parent 3c7557ee2d
commit 63569b00bc
13 changed files with 522 additions and 4 deletions
@@ -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();