From fdc44dbbacd83fdee8d49fc288c2637fcbda5499 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 24 Sep 2026 05:17:04 +0800 Subject: [PATCH] fix(webidl): unify EventListener argument conversion --- moli-renderer-v8/src/abort_signal_route.rs | 49 ++-- moli-renderer-v8/src/event_listener_args.rs | 33 +-- .../browser_api/event_listener_options.rs | 254 ++++++++++++++++++ .../src/script_vm/tests/browser_api/mod.rs | 1 + moli-renderer-v8/src/window_host.rs | 159 ++--------- moli-webidl/src/helpers.rs | 92 ++----- moli-webidl/src/lib.rs | 10 +- moli-webidl/src/types.rs | 9 +- 8 files changed, 343 insertions(+), 264 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/browser_api/event_listener_options.rs diff --git a/moli-renderer-v8/src/abort_signal_route.rs b/moli-renderer-v8/src/abort_signal_route.rs index f9e463590b..64976c071e 100644 --- a/moli-renderer-v8/src/abort_signal_route.rs +++ b/moli-renderer-v8/src/abort_signal_route.rs @@ -8,7 +8,7 @@ //! registration back to the owning store. use crate::context_bootstrap::context_host_ptr_from_global_bridge; -use crate::util::{throw_type_error, v8str}; +use crate::webidl; #[derive(Clone, Copy)] enum AbortSignalOwner { @@ -136,39 +136,30 @@ impl<'s> ResolvedAbortSignal<'s> { } } -/// Parses the `signal` member of `AddEventListenerOptions`. -/// -/// The outer `Option` distinguishes abrupt conversion from an absent member; -/// the inner `Option` distinguishes no signal from a validated Window/worker -/// signal capability. +/// Converts the final AddEventListenerOptions member after capture/once/passive. +/// A dictionary uses Get, not HasProperty: Proxy traps and inherited getters +/// must run exactly once, and their original exceptions must escape unchanged. pub(crate) fn event_listener_signal_from_options_value<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, -) -> Option>> { - if value.is_null_or_undefined() || !value.is_object() { - return Some(None); - } +) -> Result>, webidl::WebIdlError> { let Ok(options) = v8::Local::::try_from(value) else { - return Some(None); + return Ok(None); + }; + let context = webidl::Context::member("AddEventListenerOptions", "signal"); + let Some(signal_value) = webidl::property_result(scope, options, "signal", context)? else { + return Ok(None); }; - let signal_key = v8str(scope, "signal"); - let signal_value = options.get(scope, signal_key.into())?; if signal_value.is_undefined() { - return Some(None); + return Ok(None); } - let Ok(signal) = v8::Local::::try_from(signal_value) else { - throw_type_error( - scope, - "Failed to execute 'addEventListener': options.signal must be an AbortSignal.", - ); - return None; - }; - let Some(signal) = ResolvedAbortSignal::resolve(scope, signal) else { - throw_type_error( - scope, - "Failed to execute 'addEventListener': options.signal must be an AbortSignal.", - ); - return None; - }; - Some(Some(signal)) + let signal = v8::Local::::try_from(signal_value) + .ok() + .and_then(|signal| ResolvedAbortSignal::resolve(scope, signal)) + .ok_or_else(|| { + webidl::WebIdlError::custom_message( + "Failed to execute 'addEventListener': options.signal must be an AbortSignal.", + ) + })?; + Ok(Some(signal)) } diff --git a/moli-renderer-v8/src/event_listener_args.rs b/moli-renderer-v8/src/event_listener_args.rs index e0ad3375eb..5a801a4ee4 100644 --- a/moli-renderer-v8/src/event_listener_args.rs +++ b/moli-renderer-v8/src/event_listener_args.rs @@ -41,7 +41,7 @@ pub(crate) struct RemoveEventListenerArgs { pub(crate) event_type: String, #[webidl(required, converter = "callback_interface", nullable)] pub(crate) listener: Option, - #[webidl(with = remove_event_listener_options)] + #[webidl(with = webidl::event_listener_options)] pub(crate) options: webidl::EventListenerOptions, } @@ -59,35 +59,6 @@ fn add_event_listener_options<'s>( // lexical order. Finish conversion even when the callback is null. let value = args.get(index); let options = webidl::add_event_listener_options_value(scope, value)?; - let signal = event_listener_signal_from_options_value(scope, value).ok_or_else(|| { - webidl::WebIdlError::pending_exception(webidl::Context::member( - "AddEventListenerOptions", - "signal", - )) - })?; + let signal = event_listener_signal_from_options_value(scope, value)?; Ok(AddEventListenerOptions { options, signal }) } - -#[derive(webidl::WebIdlDictionary)] -#[webidl(prefix = "EventListenerOptions")] -struct RemoveEventListenerOptions { - #[webidl(default = false)] - capture: bool, -} - -fn remove_event_listener_options<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - index: i32, -) -> Result { - let value = args.get(index); - let capture = if let Ok(object) = v8::Local::::try_from(value) { - webidl::parse_dictionary_object::(scope, object)?.capture - } else { - value.boolean_value(scope) - }; - Ok(webidl::EventListenerOptions { - capture, - ..webidl::EventListenerOptions::default() - }) -} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/event_listener_options.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/event_listener_options.rs new file mode 100644 index 0000000000..c51086d9ab --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/event_listener_options.rs @@ -0,0 +1,254 @@ +use super::service_worker_drain::drain_service_worker_test_turn; +use super::*; + +const OPTIONS_PROBE: &str = r#"function checkOptions(targets) { + const rows=[];let serial=0; + for(const [targetName,create] of targets) for(const method of ['addEventListener','removeEventListener']) + for(const callbackKind of ['function','null','primitive']) for(const fail of ['none','capture','once','passive','signal']) { + const [target,eventType,cleanup]=create(++serial); + const trace=[],marker={};let count=0,error=null; + const listener=()=>count++; + const callback=callbackKind==='function'?listener:callbackKind==='null'?null:42; + if(method==='removeEventListener')target.addEventListener(eventType,listener); + const type={toString(){trace.push('type');return eventType;}}; + const options=new Proxy({}, { + has(_,name){trace.push('has:'+name);return true;}, + get(_,name){trace.push(String(name));if(name===fail)throw marker;return name==='signal'?undefined:false;} + }); + try{target[method](type,callback,options);}catch(e){error=e===marker?'marker':e.name;} + target.dispatchEvent(new Event(eventType)); + target.removeEventListener(eventType,listener); + rows.push({target:targetName,method,callback:callbackKind,fail,trace,error,count}); + if(cleanup)cleanup(); + } + return rows; +} +"#; + +const PORT_PROBE: &str = r#"async function checkPortOptions() { + const targets=[['MessagePort',()=>{const channel=new MessageChannel();return [channel.port1,'message',()=>{channel.port1.close();channel.port2.close();},()=>channel.port2.postMessage('probe')];}]]; + const rows=[];let serial=0; + for(const [targetName,create] of targets) for(const method of ['addEventListener','removeEventListener']) + for(const callbackKind of ['function','null','primitive']) for(const fail of ['none','capture','once','passive','signal']) { + const [target,eventType,cleanup,send]=create(++serial); + const trace=[],marker={};let count=0,error=null; + const listener=()=>count++; + const callback=callbackKind==='function'?listener:callbackKind==='null'?null:42; + if(method==='removeEventListener')target.addEventListener(eventType,listener); + const type={toString(){trace.push('type');return eventType;}}; + const options=new Proxy({}, { + has(_,name){trace.push('has:'+name);return true;}, + get(_,name){trace.push(String(name));if(name===fail)throw marker;return name==='signal'?undefined:false;} + }); + try{target[method](type,callback,options);}catch(e){error=e===marker?'marker':e.name;} + await new Promise(resolve=>{target.onmessage=resolve;target.start();send();}); + target.removeEventListener(eventType,listener); + rows.push({target:targetName,method,callback:callbackKind,fail,trace,error,count}); + if(cleanup)cleanup(); + } + return rows; +} +"#; + +const PAGE_PROBE: &str = r#"(() => { + const frame=document.body.appendChild(document.createElement('iframe')); + const rows=checkOptions([ + ['Window',n=>[window,'options-'+n]], + ['child Window',n=>[frame.contentWindow,'options-'+n]], + ['Document',n=>[document,'options-'+n]], + ['Element',n=>[document.createElement('div'),'options-'+n]], + ['EventTarget',n=>[new EventTarget(),'options-'+n]], + ['FileReader',n=>[new FileReader(),'options-'+n]], + ['AbortSignal',()=>[new AbortController().signal,'abort']] + ]);frame.remove();return rows; +})()"#; + +const WORKER_TARGETS: &str = r#"checkOptions([ + ['WorkerGlobalScope',n=>[self,'options-'+n]], + ['EventTarget',n=>[new EventTarget(),'options-'+n]], + ['FileReader',n=>[new FileReader(),'options-'+n]], + ['AbortSignal',()=>[new AbortController().signal,'abort']] +])"#; + +const SIGNAL_PROBE: &str = r#"(() => { + const frame=document.body.appendChild(document.createElement('iframe')); + const child=frame.contentWindow; + const channel=new MessageChannel(); + const targets=[window,child,document,document.createElement('div'),new EventTarget(),new FileReader(),new AbortController().signal,channel.port1]; + const localSignal=new AbortController().signal,childSignal=new child.AbortController().signal; + const revoked=Proxy.revocable(localSignal,{});revoked.revoke(); + const signals=[['undefined',undefined],['null',null],['number',1],['object',{}],['forged',Object.create(AbortSignal.prototype)],['proxy',new Proxy(localSignal,{})],['revoked',revoked.proxy],['local',localSignal],['child',childSignal]]; + const rows=[]; + for(let index=0;index", + ); + let value = vm + .eval(&format!("{OPTIONS_PROBE}\nJSON.stringify({PAGE_PROBE})")) + .unwrap(); + assert_option_rows(&serde_json::from_str(&value).unwrap(), 210); +} + +#[test] +fn event_listener_options_validate_signal_even_for_null_callbacks() { + let mut vm = new_parsed_test_vm( + "https://event-listener-signal-conversion.test/", + "", + ); + let value = vm.eval(&format!("JSON.stringify({SIGNAL_PROBE})")).unwrap(); + let rows: serde_json::Value = serde_json::from_str(&value).unwrap(); + let rows = rows.as_array().unwrap(); + assert_eq!(rows.len(), 144); + for row in rows { + let add = row["method"] == "addEventListener"; + let valid = matches!( + row["kind"].as_str().unwrap(), + "undefined" | "local" | "child" + ); + let expected = if add && !valid { + serde_json::json!("TypeError") + } else { + serde_json::Value::Null + }; + assert_eq!(row["error"], expected, "{row}"); + assert_eq!( + row["realm"], + if expected.is_null() { + serde_json::Value::Null + } else { + serde_json::json!(true) + }, + "{row}" + ); + assert_eq!( + row["trace"], + if add { + serde_json::json!(["capture", "once", "passive", "signal"]) + } else { + serde_json::json!(["capture"]) + }, + "{row}" + ); + } +} + +#[tokio::test] +async fn worker_event_listener_options_share_conversion_and_exception_semantics() { + for shared in [false, true] { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + let (mut vm, browser_context_runtime) = + new_service_worker_page_test_vm_with_loader_and_browser_context_runtime( + "https://worker-listener-options.test/", + &loader, + ); + let worker_source = if shared { + format!( + "{OPTIONS_PROBE}\n{PORT_PROBE}\nonconnect=async e=>e.ports[0].postMessage({{options:{WORKER_TARGETS},ports:await checkPortOptions()}});" + ) + } else { + format!( + "{OPTIONS_PROBE}\n{PORT_PROBE}\n(async()=>postMessage({{options:{WORKER_TARGETS},ports:await checkPortOptions()}}))();" + ) + }; + let worker_source = serde_json::to_string(&worker_source).unwrap(); + let constructor = if shared { "SharedWorker" } else { "Worker" }; + let port = if shared { "worker.port" } else { "worker" }; + let cleanup = if shared { + "port.close()" + } else { + "worker.terminate()" + }; + vm.eval(&format!(r#" + const url=URL.createObjectURL(new Blob([{worker_source}],{{type:'text/javascript'}})); + const worker=new {constructor}(url),port={port}; + port.onmessage=e=>{{globalThis.optionsResult=e.data;{cleanup};URL.revokeObjectURL(url);}}; + worker.onerror=e=>{{globalThis.optionsResult=String(e.message);}}; + "#)).unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(10), async { + while vm.eval("globalThis.optionsResult !== undefined").unwrap() != "true" { + browser_context_runtime.drain_shared_worker_service_lane(); + drain_service_worker_test_turn(&mut vm, &browser_context_runtime, &loader).await; + } + }) + .await + .expect("worker options probe should settle"); + let value = vm.eval("JSON.stringify(globalThis.optionsResult)").unwrap(); + let value: serde_json::Value = serde_json::from_str(&value).unwrap(); + assert_option_rows(&value["options"], 120); + assert_option_rows(&value["ports"], 30); + } +} + +#[tokio::test] +async fn message_port_options_errors_preserve_actual_message_listeners() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + let (mut vm, browser_context_runtime) = + new_service_worker_page_test_vm_with_loader_and_browser_context_runtime( + "https://message-port-options.test/", + &loader, + ); + vm.eval(&format!("{PORT_PROBE}\ncheckPortOptions().then(value=>globalThis.portOptionsResult=value,error=>globalThis.portOptionsResult=String(error));")).unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(10), async { + while vm + .eval("globalThis.portOptionsResult !== undefined") + .unwrap() + != "true" + { + drain_service_worker_test_turn(&mut vm, &browser_context_runtime, &loader).await; + } + }) + .await + .expect("message port options probe should settle"); + let value = vm + .eval("JSON.stringify(globalThis.portOptionsResult)") + .unwrap(); + assert_option_rows(&serde_json::from_str(&value).unwrap(), 30); +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index 2c00056252..2850a8e67e 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -16,6 +16,7 @@ mod crypto_subtle_x25519; mod date_locale; mod details; mod event_handlers; +mod event_listener_options; mod events_selection_storage; mod gamepad; mod ice_candidate; diff --git a/moli-renderer-v8/src/window_host.rs b/moli-renderer-v8/src/window_host.rs index 5eee87e6f7..0d37e74043 100644 --- a/moli-renderer-v8/src/window_host.rs +++ b/moli-renderer-v8/src/window_host.rs @@ -39,7 +39,7 @@ use super::{ }, script_provenance::CompiledStringProvenance, util::{ - callback_arg_string, context_host_from_global_bridge, context_host_ptr_from_global_bridge, + context_host_from_global_bridge, context_host_ptr_from_global_bridge, context_host_ptr_from_window_object, define_non_enumerable_static_bool_property, get_private_value, object_bool_property, object_number_property, script_base_url_from_continuation_data, script_base_url_from_host_defined_options, @@ -47,6 +47,9 @@ use super::{ }, webidl, }; +use crate::event_listener_args::{ + AddEventListenerArgs, RemoveEventListenerArgs, parse_listener_args, +}; use crate::web_api_interfaces; use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -142,124 +145,6 @@ struct IdleDeadlinePrototypeDeclaration { time_remaining: (), } -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "EventTarget.addEventListener")] -struct WindowAddEventListenerArgs<'s> { - #[webidl(with = window_add_event_listener_call)] - call: webidl::ParseOutcome>, -} - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "EventTarget.removeEventListener")] -struct WindowRemoveEventListenerArgs<'s> { - #[webidl(with = window_remove_event_listener_call)] - call: webidl::ParseOutcome>, -} - -struct WindowAddEventListenerCall<'s> { - event_type: String, - callback: v8::Local<'s, v8::Object>, - callback_relevant_context: v8::Local<'s, v8::Context>, - incumbent_context: v8::Local<'s, v8::Context>, - options: webidl::EventListenerOptions, - signal: Option>, -} - -struct WindowRemoveEventListenerCall<'s> { - event_type: String, - callback: v8::Local<'s, v8::Object>, - options: webidl::EventListenerOptions, -} - -fn window_add_event_listener_call<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - _index: i32, -) -> Result>, webidl::WebIdlError> { - let Some(event_type) = callback_arg_string(scope, args, 0) else { - return Ok(webidl::ParseOutcome::Skip); - }; - let options = webidl::event_listener_options(scope, args, 2, true); - let listener_arg = args.get(1); - let current_context = scope.get_current_context(); - let callback = if let Ok(function) = v8::Local::::try_from(listener_arg) { - let callback = v8::Local::::from(function); - let callback_relevant_context = callback - .get_creation_context(scope) - .unwrap_or(current_context); - Some((callback, callback_relevant_context)) - } else if listener_arg.is_object() && !listener_arg.is_null_or_undefined() { - let Ok(object) = v8::Local::::try_from(listener_arg) else { - return Ok(webidl::ParseOutcome::Skip); - }; - let callback_relevant_context = object - .get_creation_context(scope) - .unwrap_or(current_context); - Some((object, callback_relevant_context)) - } else { - None - }; - let Some((callback, callback_relevant_context)) = callback else { - return Ok(webidl::ParseOutcome::Skip); - }; - let incumbent_context = scope.get_incumbent_context().unwrap_or(current_context); - Ok(webidl::ParseOutcome::Parsed(WindowAddEventListenerCall { - event_type, - callback, - callback_relevant_context, - incumbent_context, - signal: signal_from_options_value(scope, args.get(2)), - options, - })) -} - -fn window_remove_event_listener_call<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - _index: i32, -) -> Result>, webidl::WebIdlError> { - let Some(event_type) = callback_arg_string(scope, args, 0) else { - return Ok(webidl::ParseOutcome::Skip); - }; - let options = webidl::event_listener_options(scope, args, 2, true); - let listener_arg = args.get(1); - let callback = if let Ok(function) = v8::Local::::try_from(listener_arg) { - Some(v8::Local::::from(function)) - } else if listener_arg.is_object() && !listener_arg.is_null_or_undefined() { - v8::Local::::try_from(listener_arg).ok() - } else { - None - }; - let Some(callback) = callback else { - return Ok(webidl::ParseOutcome::Skip); - }; - Ok(webidl::ParseOutcome::Parsed( - WindowRemoveEventListenerCall { - event_type, - callback, - options, - }, - )) -} - -fn signal_from_options_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, -) -> Option> { - let Ok(options) = v8::Local::::try_from(value) else { - return None; - }; - options - .get(scope, v8str(scope, "signal").into()) - .and_then(|value| { - if value.is_null_or_undefined() { - None - } else { - v8::Local::::try_from(value).ok() - } - }) -} - pub(super) fn event_target_add_event_listener_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -273,16 +158,17 @@ pub(super) fn event_target_add_event_listener_callback<'s>( return; }; let host = unsafe { &mut *host_ptr }; - let parsed = webidl::parse_args::(scope, &args); - let Some(parsed) = parsed else { + let parsed = parse_listener_args::(scope, &args, "addEventListener"); + let Some(call) = parsed else { return; }; - let webidl::ParseOutcome::Parsed(call) = parsed.call else { + let Some(listener) = call.listener else { return; }; - let capture = call.options.capture; - let once = call.options.once; - let signal = call.signal; + let options = call.options.options; + let capture = options.capture; + let once = options.once; + let signal = call.options.signal.map(|signal| signal.value()); if let Some(signal) = signal && host.abort_signal_aborted(scope, signal) { @@ -303,17 +189,20 @@ pub(super) fn event_target_add_event_listener_callback<'s>( throw_type_error(scope, "Illegal invocation"); return; }; - let passive = call - .options + let passive = options .passive .unwrap_or_else(|| default_passive_value(host, target, &call.event_type)); + let callback = v8::Local::::try_from(listener.value(scope)) + .expect("converted EventListener must remain an object"); + let callback_relevant_context = listener.relevant_context(scope); + let incumbent_context = listener.incumbent_context(scope); let Some(callback_id) = host.register_target_event_listener( scope, target, &call.event_type, - call.callback, - call.callback_relevant_context, - call.incumbent_context, + callback, + callback_relevant_context, + incumbent_context, capture, once, passive, @@ -391,10 +280,12 @@ pub(super) fn event_target_remove_event_listener_callback<'s>( return; }; let host = unsafe { &mut *host_ptr }; - let Some(parsed) = webidl::parse_args::(scope, &args) else { + let Some(call) = + parse_listener_args::(scope, &args, "removeEventListener") + else { return; }; - let webidl::ParseOutcome::Parsed(call) = parsed.call else { + let Some(listener) = call.listener else { return; }; let capture = call.options.capture; @@ -411,7 +302,9 @@ pub(super) fn event_target_remove_event_listener_callback<'s>( throw_type_error(scope, "Illegal invocation"); return; }; - host.remove_registered_event_listener(scope, target, &call.event_type, call.callback, capture); + let callback = v8::Local::::try_from(listener.value(scope)) + .expect("converted EventListener must remain an object"); + host.remove_registered_event_listener(scope, target, &call.event_type, callback, capture); } pub(super) fn event_target_dispatch_event_callback<'s>( diff --git a/moli-webidl/src/helpers.rs b/moli-webidl/src/helpers.rs index c77f4f05f1..834ed28d29 100644 --- a/moli-webidl/src/helpers.rs +++ b/moli-webidl/src/helpers.rs @@ -1,4 +1,4 @@ -use crate::types::EventListenerOptionsMembers; +use crate::types::{AddEventListenerOptionsMembers, EventListenerOptionsMembers}; use crate::{ Context, DomString, EventListenerOptions, UnrestrictedDouble, WebIdlError, legacy_optional_member, parse_dictionary_object, @@ -170,69 +170,6 @@ pub fn optional_number_property<'s>( .map(Into::into) } -/// Parses the third argument shape used by event listener registration. -/// -/// Boolean values use the legacy capture-only path. Object values are parsed as -/// `AddEventListenerOptions`. When `observe_passive` is true, the `passive` -/// member is read even if the resulting value is not otherwise needed, matching -/// sites that observe getter side effects. -pub fn event_listener_options<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - index: i32, - observe_passive: bool, -) -> EventListenerOptions { - if args.length() <= index { - return EventListenerOptions::default(); - } - event_listener_options_value(scope, args.get(index), observe_passive) -} - -pub fn event_listener_options_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, - observe_passive: bool, -) -> EventListenerOptions { - if is_nullish(value) { - return EventListenerOptions::default(); - } - if !value.is_object() || value.is_boolean() { - return EventListenerOptions { - capture: value.boolean_value(scope), - once: false, - passive: None, - }; - } - let Ok(object) = v8::Local::::try_from(value) else { - return EventListenerOptions::default(); - }; - if observe_passive { - let _ = property(scope, object, "passive"); - } - parse_dictionary_object::(scope, object) - .map(|parsed| EventListenerOptions { - capture: parsed.capture, - once: parsed.once, - passive: parsed.passive, - }) - .unwrap_or_default() -} - -pub fn event_listener_once_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, -) -> bool { - event_listener_options_value(scope, value, false).once -} - -pub fn event_listener_once_option<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - index: i32, -) -> bool { - event_listener_options(scope, args, index, false).once -} - /// Converts `(AddEventListenerOptions or boolean)` through `passive`. /// /// The caller must convert the platform-specific `signal` member next, before @@ -247,10 +184,35 @@ pub fn add_event_listener_options_value<'s>( ..EventListenerOptions::default() }); }; - let parsed = parse_dictionary_object::(scope, object)?; + let parsed = parse_dictionary_object::(scope, object)?; Ok(EventListenerOptions { capture: parsed.capture, once: parsed.once, passive: parsed.passive, }) } + +/// Converts removeEventListener's `(EventListenerOptions or boolean)` argument. +/// Only `capture` belongs to this dictionary; no registration-only getters run. +pub fn event_listener_options<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + index: i32, +) -> Result { + event_listener_options_value(scope, args.get(index)) +} + +pub fn event_listener_options_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, +) -> Result { + let capture = if let Ok(object) = v8::Local::::try_from(value) { + parse_dictionary_object::(scope, object)?.capture + } else { + value.boolean_value(scope) + }; + Ok(EventListenerOptions { + capture, + ..EventListenerOptions::default() + }) +} diff --git a/moli-webidl/src/lib.rs b/moli-webidl/src/lib.rs index 092bea0471..b3848b5fdd 100644 --- a/moli-webidl/src/lib.rs +++ b/moli-webidl/src/lib.rs @@ -49,11 +49,11 @@ pub use convert::{ }; pub use error::{Context, WebIdlError, WebIdlErrorKind}; pub use helpers::{ - add_event_listener_options_value, dictionary_arg, dictionary_value, event_listener_once_option, - event_listener_once_value, event_listener_options, event_listener_options_value, is_nullish, - optional_number_property, optional_object_arg, optional_string_property, property, - property_non_nullish, property_non_undefined, property_result, symbol_property_result, - throw_dom_exception, throw_error, throw_index_size_error, throw_type_error, v8_string, + add_event_listener_options_value, dictionary_arg, dictionary_value, event_listener_options, + event_listener_options_value, is_nullish, optional_number_property, optional_object_arg, + optional_string_property, property, property_non_nullish, property_non_undefined, + property_result, symbol_property_result, throw_dom_exception, throw_error, + throw_index_size_error, throw_type_error, v8_string, }; pub use moli_webidl_callback::{ PreparedWebIdlCallbackFunction, PreparedWebIdlCallbackInterface, WebIdlCallbackFunction, diff --git a/moli-webidl/src/types.rs b/moli-webidl/src/types.rs index 7edf3e7cbf..c3f318e5b4 100644 --- a/moli-webidl/src/types.rs +++ b/moli-webidl/src/types.rs @@ -143,10 +143,17 @@ pub struct EventListenerOptions { } #[derive(WebIdlDictionary)] -#[webidl(prefix = "AddEventListenerOptions")] +#[webidl(prefix = "EventListenerOptions")] pub(crate) struct EventListenerOptionsMembers { #[webidl(default = false)] pub(crate) capture: bool, +} + +#[derive(WebIdlDictionary)] +#[webidl(prefix = "AddEventListenerOptions")] +pub(crate) struct AddEventListenerOptionsMembers { + #[webidl(default = false)] + pub(crate) capture: bool, #[webidl(default = false)] pub(crate) once: bool, pub(crate) passive: Option,