mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 08:00:56 +00:00
fix(webidl): unify EventListener argument conversion
This commit is contained in:
@@ -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<Option<ResolvedAbortSignal<'s>>> {
|
||||
if value.is_null_or_undefined() || !value.is_object() {
|
||||
return Some(None);
|
||||
}
|
||||
) -> Result<Option<ResolvedAbortSignal<'s>>, webidl::WebIdlError> {
|
||||
let Ok(options) = v8::Local::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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))
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ pub(crate) struct RemoveEventListenerArgs {
|
||||
pub(crate) event_type: String,
|
||||
#[webidl(required, converter = "callback_interface", nullable)]
|
||||
pub(crate) listener: Option<webidl::WebIdlCallbackInterface>,
|
||||
#[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<webidl::EventListenerOptions, webidl::WebIdlError> {
|
||||
let value = args.get(index);
|
||||
let capture = if let Ok(object) = v8::Local::<v8::Object>::try_from(value) {
|
||||
webidl::parse_dictionary_object::<RemoveEventListenerOptions>(scope, object)?.capture
|
||||
} else {
|
||||
value.boolean_value(scope)
|
||||
};
|
||||
Ok(webidl::EventListenerOptions {
|
||||
capture,
|
||||
..webidl::EventListenerOptions::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<targets.length;index++)for(const method of ['addEventListener','removeEventListener'])for(const [kind,signal]of signals){
|
||||
const trace=[];let error=null,realm=null;
|
||||
const options=new Proxy({}, {get(_,key){trace.push(String(key));return key==='signal'?signal:false;},has(){throw new Error('must not call HasProperty');}});
|
||||
try{targets[index][method]('probe',null,options);}catch(e){error=e.name;realm=e instanceof (index===1?child:window).TypeError;}
|
||||
rows.push({index,method,kind,trace,error,realm});
|
||||
}
|
||||
channel.port1.close();channel.port2.close();frame.remove();return rows;
|
||||
})()"#;
|
||||
|
||||
fn assert_option_rows(value: &serde_json::Value, expected_rows: usize) {
|
||||
let rows = value.as_array().expect("options probe must return rows");
|
||||
assert_eq!(rows.len(), expected_rows);
|
||||
for row in rows {
|
||||
let add = row["method"] == "addEventListener";
|
||||
let primitive = row["callback"] == "primitive";
|
||||
let callable = row["callback"] == "function";
|
||||
let fail = row["fail"].as_str().unwrap();
|
||||
let members: &[&str] = if add {
|
||||
&["capture", "once", "passive", "signal"]
|
||||
} else {
|
||||
&["capture"]
|
||||
};
|
||||
let mut trace = vec!["type"];
|
||||
let mut error = None;
|
||||
if primitive {
|
||||
error = Some("TypeError");
|
||||
} else {
|
||||
for member in members {
|
||||
trace.push(member);
|
||||
if *member == fail {
|
||||
error = Some("marker");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let count = if add {
|
||||
usize::from(callable && error.is_none())
|
||||
} else {
|
||||
usize::from(!callable || error.is_some())
|
||||
};
|
||||
assert_eq!(row["trace"], serde_json::json!(trace), "{row}");
|
||||
assert_eq!(row["error"], serde_json::json!(error), "{row}");
|
||||
assert_eq!(row["count"], serde_json::json!(count), "{row}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_listener_options_convert_in_order_and_stop_before_mutation() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://event-listener-options.test/",
|
||||
"<!doctype html><html><body></body></html>",
|
||||
);
|
||||
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/",
|
||||
"<!doctype html><html><body></body></html>",
|
||||
);
|
||||
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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<WindowAddEventListenerCall<'s>>,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "EventTarget.removeEventListener")]
|
||||
struct WindowRemoveEventListenerArgs<'s> {
|
||||
#[webidl(with = window_remove_event_listener_call)]
|
||||
call: webidl::ParseOutcome<WindowRemoveEventListenerCall<'s>>,
|
||||
}
|
||||
|
||||
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<v8::Local<'s, v8::Object>>,
|
||||
}
|
||||
|
||||
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::ParseOutcome<WindowAddEventListenerCall<'s>>, 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::<v8::Function>::try_from(listener_arg) {
|
||||
let callback = v8::Local::<v8::Object>::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::<v8::Object>::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::ParseOutcome<WindowRemoveEventListenerCall<'s>>, 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::<v8::Function>::try_from(listener_arg) {
|
||||
Some(v8::Local::<v8::Object>::from(function))
|
||||
} else if listener_arg.is_object() && !listener_arg.is_null_or_undefined() {
|
||||
v8::Local::<v8::Object>::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<v8::Local<'s, v8::Object>> {
|
||||
let Ok(options) = v8::Local::<v8::Object>::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::<v8::Object>::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::<WindowAddEventListenerArgs>(scope, &args);
|
||||
let Some(parsed) = parsed else {
|
||||
let parsed = parse_listener_args::<AddEventListenerArgs>(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::<v8::Object>::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::<WindowRemoveEventListenerArgs>(scope, &args) else {
|
||||
let Some(call) =
|
||||
parse_listener_args::<RemoveEventListenerArgs>(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::<v8::Object>::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>(
|
||||
|
||||
+27
-65
@@ -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::<v8::Object>::try_from(value) else {
|
||||
return EventListenerOptions::default();
|
||||
};
|
||||
if observe_passive {
|
||||
let _ = property(scope, object, "passive");
|
||||
}
|
||||
parse_dictionary_object::<EventListenerOptionsMembers>(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::<EventListenerOptionsMembers>(scope, object)?;
|
||||
let parsed = parse_dictionary_object::<AddEventListenerOptionsMembers>(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<EventListenerOptions, WebIdlError> {
|
||||
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<EventListenerOptions, WebIdlError> {
|
||||
let capture = if let Ok(object) = v8::Local::<v8::Object>::try_from(value) {
|
||||
parse_dictionary_object::<EventListenerOptionsMembers>(scope, object)?.capture
|
||||
} else {
|
||||
value.boolean_value(scope)
|
||||
};
|
||||
Ok(EventListenerOptions {
|
||||
capture,
|
||||
..EventListenerOptions::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<bool>,
|
||||
|
||||
Reference in New Issue
Block a user