fix(events): preserve KeyboardEvent initialization semantics

This commit is contained in:
ldm0
2026-10-16 11:13:17 +08:00
committed by Donough Liu
parent 416b8dbf4b
commit 31605ab1d5
12 changed files with 676 additions and 165 deletions
@@ -124,33 +124,6 @@ struct InitMouseEventArgs<'s> {
related_target: Option<v8::Local<'s, v8::Object>>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "KeyboardEvent.initKeyboardEvent")]
struct InitKeyboardEventArgs<'s> {
#[webidl(
required,
converter = "raw",
missing_message = "Failed to execute 'initKeyboardEvent': 1 argument required."
)]
event_type: webidl::DomString16,
#[webidl(default = false)]
bubbles: bool,
#[webidl(default = false)]
cancelable: bool,
#[webidl(index = 3, converter = "raw")]
view: Option<v8::Local<'s, v8::Value>>,
#[webidl(default = "", index = 4)]
key: String,
#[webidl(default = 0, index = 5)]
location: i32,
#[webidl(default = "", index = 6)]
_modifiers_list: String,
#[webidl(default = false, index = 7)]
repeat: bool,
#[webidl(default = "", index = 8)]
_locale: String,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "TextEvent.initTextEvent")]
struct InitTextEventArgs<'s> {
@@ -189,13 +162,6 @@ struct InitCompositionEventArgs<'s> {
data: String,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "KeyboardEvent.getModifierState")]
struct KeyboardEventGetModifierStateArgs {
#[webidl(required)]
key_arg: String,
}
#[derive(WebApiObject)]
#[webapi(plain, data_properties, enumerable)]
struct LegacyUiEventInitDeclaration<'scope> {
@@ -243,28 +209,6 @@ struct LegacyMouseEventTailInitDeclaration<'scope> {
related_target: v8::Local<'scope, v8::Value>,
}
#[derive(WebApiObject)]
#[webapi(plain, data_properties, enumerable)]
struct LegacyKeyboardEventInitDeclaration<'scope> {
view: v8::Local<'scope, v8::Value>,
#[webapi(constructor_default = 0)]
detail: i32,
key: v8::Local<'scope, v8::String>,
code: v8::Local<'scope, v8::String>,
location: i32,
repeat: bool,
#[webapi(constructor_default = false)]
is_composing: bool,
#[webapi(constructor_default = false)]
ctrl_key: bool,
#[webapi(constructor_default = false)]
shift_key: bool,
#[webapi(constructor_default = false)]
alt_key: bool,
#[webapi(constructor_default = false)]
meta_key: bool,
}
#[derive(WebApiObject)]
#[webapi(plain, data_properties, enumerable)]
struct LegacyCustomEventInitDeclaration<'scope> {
@@ -420,32 +364,6 @@ pub(super) fn mouse_event_init_callback<'s>(
.expect("legacy MouseEvent tail init declaration should initialize");
}
pub(super) fn keyboard_event_init_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let event = super::events::event_backing(scope, args.this());
let Some(parsed) = webidl::parse_args::<InitKeyboardEventArgs>(scope, &args) else {
return;
};
let view = legacy_event_view_or_global(scope, parsed.view);
if !reinitialize_event_object(
scope,
event,
&parsed.event_type.0,
parsed.bubbles,
parsed.cancelable,
) {
return;
}
let key = v8_string(scope, &parsed.key).unwrap_or_else(|| v8str(scope, ""));
let code = v8str(scope, "");
LegacyKeyboardEventInitDeclaration::new(view, key, code, parsed.location, parsed.repeat)
.initialize(scope, event)
.expect("legacy KeyboardEvent init declaration should initialize");
}
pub(super) fn composition_event_init_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -523,38 +441,3 @@ pub(super) fn storage_event_init_callback<'s>(
parsed.storage_area.map(|value| value.0.into()),
);
}
pub(super) fn keyboard_event_get_modifier_state_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<KeyboardEventGetModifierStateArgs>(scope, &args) else {
return;
};
let event = super::events::event_backing(scope, args.this());
let key_name = match parsed.key_arg.as_str() {
"Alt" | "AltGraph" => "altKey",
"Control" => "ctrlKey",
"Shift" => "shiftKey",
"Meta" => "metaKey",
"Accel" => {
let ctrl = event
.get(scope, v8str(scope, "ctrlKey").into())
.is_some_and(|value| value.boolean_value(scope));
let meta = event
.get(scope, v8str(scope, "metaKey").into())
.is_some_and(|value| value.boolean_value(scope));
rv.set(v8::Boolean::new(scope, ctrl || meta).into());
return;
}
_ => {
rv.set(v8::Boolean::new(scope, false).into());
return;
}
};
let value = event
.get(scope, v8str(scope, key_name).into())
.is_some_and(|value| value.boolean_value(scope));
rv.set(v8::Boolean::new(scope, value).into());
}
@@ -2,7 +2,6 @@ use super::{
event_document::{document_create_event_callback, document_has_focus_callback},
event_legacy::{
composition_event_init_callback, custom_event_init_callback, event_init_event_callback,
keyboard_event_get_modifier_state_callback, keyboard_event_init_callback,
mouse_event_init_callback, storage_event_init_callback, text_event_init_callback,
ui_event_init_callback,
},
@@ -13,16 +12,18 @@ use super::{
event_cancelable_getter_function, event_composed_getter_function,
event_composed_path_callback, event_current_target_getter_function,
event_default_prevented_getter_function, event_event_phase_getter_function,
event_get_modifier_state_callback, event_platform_attribute_getter,
event_prevent_default_callback, event_return_value_getter_function,
event_return_value_setter_function, event_src_element_getter_function,
event_stop_immediate_propagation_callback, event_stop_propagation_callback,
event_target_getter_function, event_time_stamp_getter_function, event_type_getter_function,
event_value_attribute_getter, focus_event_related_target_getter_function,
form_data_event_form_data_getter_function, message_event_init_callback,
mouse_event_related_target_getter_function, pointer_event_get_predicted_events_callback,
submit_event_agent_invoked_getter_function, submit_event_respond_with_callback,
submit_event_submitter_getter_function, track_event_track_getter_function,
ui_event_pseudo_target_getter_function, ui_event_which_getter_function,
form_data_event_form_data_getter_function, keyboard_event_init_callback,
message_event_init_callback, mouse_event_related_target_getter_function,
pointer_event_get_predicted_events_callback, submit_event_agent_invoked_getter_function,
submit_event_respond_with_callback, submit_event_submitter_getter_function,
track_event_track_getter_function, ui_event_pseudo_target_getter_function,
ui_event_which_getter_function,
},
selection_surface::document_get_selection_callback,
specs::ConstructorSpec,
@@ -166,7 +167,7 @@ struct FormDataEventTemplateAccessorsDeclaration {
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::UIEvent, enumerable, receiver)]
struct UiEventTemplateMethodsDeclaration {
#[webapi(accessor_property = "view", getter = event_value_attribute_getter, data = crate::util::v8str(scope, "view"))]
#[webapi(accessor_property = "view", getter = event_platform_attribute_getter, data = crate::util::v8str(scope, "view"))]
view: (),
#[webapi(accessor_property = "detail", getter = event_value_attribute_getter, data = crate::util::v8str(scope, "detail"))]
detail: (),
@@ -301,8 +302,8 @@ struct KeyboardEventTemplateMethodsDeclaration {
#[webapi(
method = "getModifierState",
length = 0,
callback = keyboard_event_get_modifier_state_callback
length = 1,
callback = event_get_modifier_state_callback
)]
get_modifier_state: (),
}
@@ -9,8 +9,10 @@ mod kind;
mod message;
pub(crate) use message::construct_original_message_event;
mod methods;
mod modifiers;
mod subclasses;
mod submit;
mod ui;
mod value;
mod wrappers;
@@ -499,7 +501,12 @@ pub(crate) use wrappers::{
};
pub(in crate::context_bootstrap) use message::message_event_init_callback;
pub(in crate::context_bootstrap) use wrappers::event_value_attribute_getter;
pub(in crate::context_bootstrap) use wrappers::{
event_platform_attribute_getter, event_value_attribute_getter,
};
pub(in crate::context_bootstrap) use modifiers::event_get_modifier_state_callback;
pub(in crate::context_bootstrap) use subclasses::keyboard_event_init_callback;
pub(crate) use subclasses::construct_original_error_event;
@@ -0,0 +1,153 @@
use super::*;
use crate::webidl;
const EVENT_MODIFIERS_SLOT: &str = "__moliEventModifiers";
const MODIFIER_KEYS: [&str; 14] = [
"Alt",
"Control",
"Meta",
"AltGraph",
"CapsLock",
"Fn",
"FnLock",
"Hyper",
"NumLock",
"ScrollLock",
"Super",
"Symbol",
"SymbolLock",
"Shift",
];
/// EventModifierInit members, in Web IDL dictionary order.
#[derive(Default, webidl::WebIdlDictionary)]
#[webidl(prefix = "EventModifierInit")]
pub(super) struct EventModifierInitMembers {
#[webidl(default = false)]
alt_key: bool,
#[webidl(default = false)]
ctrl_key: bool,
#[webidl(default = false)]
meta_key: bool,
#[webidl(default = false)]
modifier_alt_graph: bool,
#[webidl(default = false)]
modifier_caps_lock: bool,
#[webidl(default = false)]
modifier_fn: bool,
#[webidl(default = false)]
modifier_fn_lock: bool,
#[webidl(default = false)]
modifier_hyper: bool,
#[webidl(default = false)]
modifier_num_lock: bool,
#[webidl(default = false)]
modifier_scroll_lock: bool,
#[webidl(default = false)]
modifier_super: bool,
#[webidl(default = false)]
modifier_symbol: bool,
#[webidl(default = false)]
modifier_symbol_lock: bool,
#[webidl(default = false)]
shift_key: bool,
}
#[derive(WebApiObject)]
#[webapi(plain, data_properties, enumerable)]
struct EventModifierProperties {
alt_key: bool,
ctrl_key: bool,
meta_key: bool,
shift_key: bool,
}
impl EventModifierInitMembers {
pub(super) fn initialize<'s>(
&self,
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
) {
let event = event_backing(scope, event);
let values = [
self.alt_key,
self.ctrl_key,
self.meta_key,
self.modifier_alt_graph,
self.modifier_caps_lock,
self.modifier_fn,
self.modifier_fn_lock,
self.modifier_hyper,
self.modifier_num_lock,
self.modifier_scroll_lock,
self.modifier_super,
self.modifier_symbol,
self.modifier_symbol_lock,
self.shift_key,
];
let bits = values
.into_iter()
.enumerate()
.fold(0, |bits, (index, active)| {
bits | (u32::from(active) << index)
});
set_private_value(
scope,
event,
EVENT_MODIFIERS_SLOT,
v8::Integer::new_from_unsigned(scope, bits).into(),
);
EventModifierProperties::new(self.alt_key, self.ctrl_key, self.meta_key, self.shift_key)
.initialize(scope, event)
.expect("event modifier properties should initialize");
}
}
pub(in crate::context_bootstrap) fn initialize_legacy_event_modifiers<'s>(
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
ctrl_key: bool,
alt_key: bool,
shift_key: bool,
meta_key: bool,
) {
EventModifierInitMembers {
ctrl_key,
alt_key,
shift_key,
meta_key,
..Default::default()
}
.initialize(scope, event);
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "getModifierState")]
struct GetModifierStateArgs {
#[webidl(required)]
key_arg: String,
}
pub(in crate::context_bootstrap) fn event_get_modifier_state_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<GetModifierStateArgs>(scope, &args) else {
return;
};
// Retain the legacy accelerator alias for the native platform.
let key = match parsed.key_arg.as_str() {
"Accel" if cfg!(target_os = "macos") => "Meta",
"Accel" => "Control",
key => key,
};
let bits = event_private_value(scope, args.this(), EVENT_MODIFIERS_SLOT)
.and_then(|value| value.uint32_value(scope))
.unwrap_or_default();
let active = MODIFIER_KEYS
.iter()
.position(|name| *name == key)
.is_some_and(|index| bits & (1 << index) != 0);
rv.set(v8::Boolean::new(scope, active).into());
}
@@ -26,3 +26,4 @@ pub(in crate::context_bootstrap) use pointer::{
pub(in crate::context_bootstrap::events) use data::initialize_navigate_event_methods;
pub(crate) use error::construct_original_error_event;
pub(in crate::context_bootstrap) use keyboard::keyboard_event_init_callback;
@@ -51,6 +51,14 @@ fn event_subclass_constructor_callback<'s>(
} else {
None
};
let keyboard_event_init = if kind == EventSubclassKind::KeyboardEvent {
let Some(init) = keyboard::parse_keyboard_event_init(scope, &args) else {
return;
};
Some(init)
} else {
None
};
let security_policy_init = if kind == EventSubclassKind::SecurityPolicyViolationEvent {
let Some(init) = security_policy::parse_security_policy_violation_event_init(scope, &args)
else {
@@ -80,6 +88,11 @@ fn event_subclass_constructor_callback<'s>(
.as_ref()
.map(security_policy::SecurityPolicyViolationEventInit::event_flags)
.or_else(|| error_init.as_ref().map(error::ErrorEventInit::event_flags))
.or_else(|| {
keyboard_event_init
.as_ref()
.map(keyboard::KeyboardEventInit::event_flags)
})
.or_else(|| {
storage_event_init
.as_ref()
@@ -141,9 +154,11 @@ fn event_subclass_constructor_callback<'s>(
}
}
EventSubclassKind::KeyboardEvent => {
if !keyboard::initialize_keyboard_event(scope, event, init) {
return;
}
keyboard::initialize_keyboard_event(
scope,
event,
keyboard_event_init.expect("KeyboardEvent init should be parsed"),
);
}
EventSubclassKind::InputEvent => data::initialize_input_event(scope, event, init),
EventSubclassKind::WheelEvent => {
@@ -1,56 +1,179 @@
use super::super::ui::{UiEventInit, initialize_legacy_ui_event};
use super::*;
use crate::webidl;
use moli_webapi_declare::WebApiObject;
/// Own KeyboardEventInit members, including the legacy code members.
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "KeyboardEventInit")]
struct KeyboardEventInitMembers {
#[webidl(default = 0)]
char_code: u32,
#[webidl(default = webidl::DomString16(Vec::new()), converter = "raw")]
code: webidl::DomString16,
#[webidl(default = false)]
is_composing: bool,
#[webidl(default = webidl::DomString16(Vec::new()), converter = "raw")]
key: webidl::DomString16,
#[webidl(default = 0)]
key_code: u32,
#[webidl(default = 0)]
location: u32,
#[webidl(default = false)]
repeat: bool,
}
impl Default for KeyboardEventInitMembers {
fn default() -> Self {
Self {
char_code: 0,
code: webidl::DomString16(Vec::new()),
is_composing: false,
key: webidl::DomString16(Vec::new()),
key_code: 0,
location: 0,
repeat: false,
}
}
}
#[derive(Default)]
pub(super) struct KeyboardEventInit<'s> {
ui: UiEventInit<'s>,
modifiers: super::super::modifiers::EventModifierInitMembers,
keyboard: KeyboardEventInitMembers,
}
impl KeyboardEventInit<'_> {
pub(super) fn event_flags(&self) -> (bool, bool, bool) {
self.ui.event_flags()
}
}
pub(super) fn parse_keyboard_event_init<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
) -> Option<KeyboardEventInit<'s>> {
let parsed = webidl::dictionary_arg(args, 1, webidl::Context::argument("KeyboardEvent", 2))
.and_then(|object| {
let Some(object) = object else {
return Ok(KeyboardEventInit::default());
};
// Parse the inheritance chain base-first. Each group's members are
// read once in lexical order, before initializing any event state.
Ok(KeyboardEventInit {
ui: webidl::parse_dictionary_object(scope, object)?,
modifiers: webidl::parse_dictionary_object(scope, object)?,
keyboard: webidl::parse_dictionary_object(scope, object)?,
})
});
match parsed {
Ok(init) => Some(init),
Err(error) => {
webidl::throw_error(scope, &error);
None
}
}
}
#[derive(WebApiObject)]
#[webapi(plain, data_properties, enumerable)]
struct KeyboardEventInitDeclaration<'scope> {
view: v8::Local<'scope, v8::Value>,
detail: f64,
key: v8::Local<'scope, v8::String>,
code: v8::Local<'scope, v8::String>,
location: f64,
char_code: f64,
key_code: f64,
which: f64,
location: u32,
char_code: u32,
key_code: u32,
repeat: bool,
is_composing: bool,
ctrl_key: bool,
shift_key: bool,
alt_key: bool,
meta_key: bool,
}
pub(in crate::context_bootstrap::events::subclasses) fn initialize_keyboard_event<'s>(
pub(super) fn initialize_keyboard_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
init: Option<v8::Local<'s, v8::Object>>,
) -> bool {
let Ok(view) = init_window_view_property(scope, init, "KeyboardEvent") else {
return false;
};
let detail = init_number_property(scope, init, "detail", 0.0);
let key = init_string_property(scope, init, "key", "");
let code = init_string_property(scope, init, "code", "");
let key = v8_string(scope, &key).expect("KeyboardEvent key");
let code = v8_string(scope, &code).expect("KeyboardEvent code");
let location = init_number_property(scope, init, "location", 0.0);
parsed: KeyboardEventInit<'s>,
) {
parsed.ui.initialize(scope, event);
let key = v8_string_from_utf16_units(scope, &parsed.keyboard.key.0).expect("KeyboardEvent key");
let code =
v8_string_from_utf16_units(scope, &parsed.keyboard.code.0).expect("KeyboardEvent code");
parsed.modifiers.initialize(scope, event);
KeyboardEventInitDeclaration::new(
view,
detail,
key,
code,
location,
init_number_property(scope, init, "charCode", 0.0),
init_number_property(scope, init, "keyCode", 0.0),
init_number_property(scope, init, "which", 0.0),
init_bool_property(scope, init, "repeat", false),
init_bool_property(scope, init, "isComposing", false),
init_bool_property(scope, init, "ctrlKey", false),
init_bool_property(scope, init, "shiftKey", false),
init_bool_property(scope, init, "altKey", false),
init_bool_property(scope, init, "metaKey", false),
parsed.keyboard.location,
parsed.keyboard.char_code,
parsed.keyboard.key_code,
parsed.keyboard.repeat,
parsed.keyboard.is_composing,
)
.initialize(scope, event)
.expect("KeyboardEvent init declaration should initialize");
true
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "KeyboardEvent.initKeyboardEvent")]
struct InitKeyboardEventArgs<'s> {
#[webidl(required, converter = "raw")]
event_type: webidl::DomString16,
#[webidl(default = false)]
bubbles: bool,
#[webidl(default = false)]
cancelable: bool,
#[webidl(nullable, interface = crate::web_api_interfaces::Window, brand_check = crate::context_bootstrap::is_window_receiver)]
view: Option<v8::Local<'s, v8::Object>>,
#[webidl(default = webidl::DomString16(Vec::new()), converter = "raw")]
key: webidl::DomString16,
#[webidl(default = 0)]
location: u32,
#[webidl(default = false)]
ctrl_key: bool,
#[webidl(default = false)]
alt_key: bool,
#[webidl(default = false)]
shift_key: bool,
#[webidl(default = false)]
meta_key: bool,
}
#[derive(WebApiObject)]
#[webapi(plain, data_properties, enumerable)]
struct LegacyKeyboardEventInitDeclaration<'scope> {
key: v8::Local<'scope, v8::String>,
location: u32,
#[webapi(constructor_default = false)]
repeat: bool,
}
pub(in crate::context_bootstrap) fn keyboard_event_init_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<InitKeyboardEventArgs>(scope, &args) else {
return;
};
let event = event_backing(scope, args.this());
if !reinitialize_event_object(
scope,
event,
&parsed.event_type.0,
parsed.bubbles,
parsed.cancelable,
) {
return;
}
initialize_legacy_ui_event(scope, event, parsed.view, 0);
let key = v8_string_from_utf16_units(scope, &parsed.key.0).expect("KeyboardEvent key");
super::super::modifiers::initialize_legacy_event_modifiers(
scope,
event,
parsed.ctrl_key,
parsed.alt_key,
parsed.shift_key,
parsed.meta_key,
);
LegacyKeyboardEventInitDeclaration::new(key, parsed.location)
.initialize(scope, event)
.expect("legacy KeyboardEvent init declaration should initialize");
}
@@ -0,0 +1,87 @@
use crate::webidl;
use moli_webapi_declare::WebApiObject;
fn nullable_window_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: Option<v8::Local<'s, v8::Object>>,
) -> v8::Local<'s, v8::Value> {
window
.map(Into::into)
.unwrap_or_else(|| v8::null(scope).into())
}
/// Own UIEventInit members, including legacy which, in dictionary order.
#[derive(Default, webidl::WebIdlDictionary)]
#[webidl(prefix = "UIEventInit")]
struct UiEventInitMembers<'s> {
#[webidl(default = 0)]
detail: i32,
#[webidl(nullable, interface = crate::web_api_interfaces::Window, brand_check = crate::context_bootstrap::is_window_receiver)]
view: Option<v8::Local<'s, v8::Object>>,
#[webidl(default = 0)]
which: u32,
}
#[derive(Default)]
pub(super) struct UiEventInit<'s> {
flags: (bool, bool, bool),
members: UiEventInitMembers<'s>,
}
impl<'s> webidl::WebIdlDictionary<'s> for UiEventInit<'s> {
fn parse_dictionary(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Result<Self, webidl::WebIdlError> {
// Read inherited members before own members. Derived dictionaries
// delegate here so each observable getter runs once, base-first.
Ok(Self {
flags: super::init::parse_event_init(scope, Some(object))?,
members: webidl::parse_dictionary_object(scope, object)?,
})
}
}
impl<'s> UiEventInit<'s> {
pub(super) fn event_flags(&self) -> (bool, bool, bool) {
self.flags
}
pub(super) fn initialize(
self,
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
) {
let view = nullable_window_value(scope, self.members.view);
UiEventState::new(view, self.members.detail, self.members.which)
.initialize(scope, event)
.expect("UIEvent state should initialize");
}
}
#[derive(WebApiObject)]
#[webapi(plain, data_properties, enumerable)]
struct UiEventState<'s> {
view: v8::Local<'s, v8::Value>,
detail: i32,
which: u32,
}
#[derive(WebApiObject)]
#[webapi(plain, data_properties, enumerable)]
struct LegacyUiEventState<'s> {
view: v8::Local<'s, v8::Value>,
detail: i32,
}
pub(in crate::context_bootstrap) fn initialize_legacy_ui_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
view: Option<v8::Local<'s, v8::Object>>,
detail: i32,
) {
let view = nullable_window_value(scope, view);
LegacyUiEventState::new(view, detail)
.initialize(scope, event)
.expect("legacy UIEvent state should initialize");
}
@@ -248,6 +248,14 @@ pub(in crate::context_bootstrap) fn event_value_attribute_getter<'s>(
event_attribute_getter::<0>(scope, args, rv);
}
pub(in crate::context_bootstrap) fn event_platform_attribute_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'_, v8::Value>,
) {
event_attribute_getter::<2>(scope, args, rv);
}
pub(super) fn event_attribute_in_wrapper<'s>(
scope: &mut v8::PinScope<'s, '_>,
wrapper: v8::Local<'s, v8::Object>,
@@ -0,0 +1,216 @@
(() => {
const rows = [], observed = {};
const check = (name, run) => {
try {
if (run() !== true) throw new Error('assertion failed');
rows.push({name, passed: true});
} catch (error) {
rows.push({name, passed: false, error: String(error), stack: error.stack});
}
};
const typeError = (run, realm = globalThis) => {
try { run(); } catch (error) { return error instanceof realm.TypeError; }
return false;
};
const throws = (run, sentinel) => {
try { run(); } catch (error) { return error === sentinel; }
return false;
};
const other = document.getElementById('child').contentWindow;
const legacy = (key, location = 0, view = null) => {
const event = new KeyboardEvent('before');
event.initKeyboardEvent('after', false, false, view, key, location);
return event;
};
const snapshot = event => JSON.stringify([
event.type, event.bubbles, event.cancelable, event.composed, event.defaultPrevented,
event.detail, event.key, event.code, event.location, event.which, event.charCode,
event.keyCode, event.repeat, event.isComposing, event.ctrlKey, event.altKey,
event.shiftKey, event.metaKey, event.getModifierState('AltGraph')
]);
const texts = ['', '\ud800', '\udc00', '\ud83d\ude00', 'a\ud800b\udc00\u0000\ud83d\ude00'];
for (let i = 0; i < texts.length; i++) {
const text = texts[i];
for (const field of ['key', 'code']) {
check(`constructor ${field} UTF-16 ${i}`, () => new KeyboardEvent('x', {[field]: text})[field] === text);
}
check(`legacy key UTF-16 ${i}`, () => legacy(text).key === text);
}
for (const [value, expected] of [[undefined, ''], [null, 'null'], [23, '23'], [true, 'true']]) {
for (const field of ['key', 'code']) {
check(`constructor ${field} ${String(value)}`, () => new KeyboardEvent('x', {[field]: value})[field] === expected);
}
check(`legacy key ${String(value)}`, () => legacy(value).key === expected);
}
for (const field of ['key', 'code']) {
check(`constructor ${field} string hint once`, () => {
let count = 0;
const value = {[Symbol.toPrimitive](hint) { if (hint !== 'string') throw new Error(hint); count++; return texts[4]; }};
return new KeyboardEvent('x', {[field]: value})[field] === texts[4] && count === 1;
});
check(`constructor ${field} Symbol rejects`, () => typeError(() => new KeyboardEvent('x', {[field]: Symbol()})));
}
check('legacy key string hint once', () => {
let count = 0;
const value = {[Symbol.toPrimitive](hint) { if (hint !== 'string') throw new Error(hint); count++; return texts[4]; }};
return legacy(value).key === texts[4] && count === 1;
});
check('legacy Symbol key rejects', () => typeError(() => legacy(Symbol())));
check('constructor default payload', () => {
const e = new KeyboardEvent('x');
return e.view === null && e.detail === 0 && e.key === '' && e.code === '' && e.location === 0 &&
e.charCode === 0 && e.keyCode === 0 && e.which === 0 && !e.repeat && !e.isComposing && !e.isTrusted;
});
for (const value of [undefined, null]) {
check(`nullish dictionary ${String(value)}`, () => new KeyboardEvent('x', value).view === null);
}
for (const value of [false, 1, '', Symbol(), 1n]) {
check(`invalid dictionary ${typeof value}`, () => typeError(() => new KeyboardEvent('x', value)));
}
for (const [field, value, expected] of [
['detail', 4.9, 4], ['detail', -4.9, -4], ['detail', 2147483648, -2147483648],
['location', -1, 4294967295], ['location', 4294967297, 1], ['location', 1.9, 1],
['charCode', -1, 4294967295], ['keyCode', -1, 4294967295], ['which', -1, 4294967295],
]) {
check(`constructor ${field} integer ${value}`, () => new KeyboardEvent('x', {[field]: value})[field] === expected);
}
for (const field of ['detail', 'location', 'charCode', 'keyCode', 'which']) {
for (const value of [NaN, Infinity, -Infinity]) {
check(`constructor ${field} nonfinite ${value}`, () => new KeyboardEvent('x', {[field]: value})[field] === 0);
}
for (const value of [1n, Symbol()]) {
check(`constructor ${field} rejects ${typeof value}`, () => typeError(() => new KeyboardEvent('x', {[field]: value})));
}
}
for (const [value, expected] of [[-1, 4294967295], [4294967297, 1], [1.9, 1], [NaN, 0], [Infinity, 0]]) {
check(`legacy location integer ${value}`, () => legacy('', value).location === expected);
}
check('legacy BigInt location rejects', () => typeError(() => legacy('', 1n)));
const modifierMembers = ['altKey', 'ctrlKey', 'metaKey', 'modifierAltGraph', 'modifierCapsLock',
'modifierFn', 'modifierFnLock', 'modifierHyper', 'modifierNumLock', 'modifierScrollLock',
'modifierSuper', 'modifierSymbol', 'modifierSymbolLock', 'shiftKey'];
const order = ['bubbles', 'cancelable', 'composed', 'detail', 'view', 'which', ...modifierMembers,
'charCode', 'code', 'isComposing', 'key', 'keyCode', 'location', 'repeat'];
check('complete inherited dictionary order', () => {
const seen = [];
new KeyboardEvent('x', new Proxy({}, {get(_target, name) { seen.push(name); }}));
observed.dictionaryReads = seen;
return seen.join() === order.join();
});
for (const stop of order) {
check(`dictionary getter exception stops at ${stop}`, () => {
const sentinel = {};
const seen = [];
const init = new Proxy({}, {get(_target, name) { seen.push(name); if (name === stop) throw sentinel; }});
return throws(() => new KeyboardEvent('x', init), sentinel) && seen.join() === order.slice(0, order.indexOf(stop) + 1).join();
});
}
for (const field of ['detail', 'which', 'code', 'key', 'charCode', 'keyCode', 'location']) {
check(`constructor ${field} conversion exception stops`, () => {
const sentinel = {}, seen = [];
const init = new Proxy({}, {get(_target, name) {
seen.push(name);
if (name === field) return {[Symbol.toPrimitive]() { throw sentinel; }};
}});
return throws(() => new KeyboardEvent('x', init), sentinel) && seen.join() === order.slice(0, order.indexOf(field) + 1).join();
});
}
check('constructor type converts before dictionary', () => {
const seen = [];
const type = {toString() {seen.push('type'); return texts[4];}};
const e = new KeyboardEvent(type, new Proxy({}, {get(_target, name) {seen.push(name);}}));
return e.type === texts[4] && seen.join() === ['type', ...order].join();
});
check('constructor inherits dictionary members', () => {
const e = new KeyboardEvent('x', Object.create({key: texts[4], code: texts[3], location: -1, view: other, ctrlKey: true}));
return e.key === texts[4] && e.code === texts[3] && e.location === 4294967295 && e.view === other && e.getModifierState('Control');
});
const views = [['window', window], ['child', other], ['null', null], ['undefined', undefined]];
for (const [name, view] of views) {
const expected = view === undefined ? null : view;
check(`constructor accepts ${name} view`, () => new KeyboardEvent('x', {view}).view === expected);
check(`legacy accepts ${name} view`, () => legacy('', 0, view).view === expected);
}
let traps = 0;
const handler = {get() {traps++; throw new Error('get trap');}, getPrototypeOf() {traps++; throw new Error('prototype trap');}};
const revoked = Proxy.revocable(window, handler); revoked.revoke();
for (const [name, view] of [['ordinary', {}], ['prototype', Object.create(window)], ['proxy', new Proxy(window, handler)], ['revoked', revoked.proxy], ['number', 1]]) {
check(`constructor rejects ${name} view before later conversion`, () => {
let converted = false;
const result = typeError(() => new KeyboardEvent('x', {view, code: {toString() {converted = true; return '';}}}));
return result && !converted && traps === 0;
});
check(`legacy rejects ${name} view before key conversion`, () => {
let converted = false;
const result = typeError(() => legacy({toString() {converted = true; return '';}}, 0, view));
return result && !converted && traps === 0;
});
}
check('legacy method descriptor', () => {
const d = Object.getOwnPropertyDescriptor(KeyboardEvent.prototype, 'initKeyboardEvent');
return d.value.name === 'initKeyboardEvent' && d.value.length === 1 && d.enumerable && d.configurable && d.writable;
});
check('legacy missing required type', () => typeError(() => new KeyboardEvent('x').initKeyboardEvent()));
check('legacy modifier booleans do not invoke author conversion', () => {
const e = new KeyboardEvent('x', {modifierAltGraph: true, modifierCapsLock: true});
const truthy = {[Symbol.toPrimitive]() {throw new Error('boolean conversion');}};
e.initKeyboardEvent('y', false, false, null, '', 0, truthy, false, truthy, false);
return e.ctrlKey && !e.altKey && e.shiftKey && !e.metaKey && e.getModifierState('Control') &&
e.getModifierState('Shift') && !e.getModifierState('AltGraph') && !e.getModifierState('CapsLock');
});
check('legacy payload defaults preserve unrelated code and composing state', () => {
const e = new KeyboardEvent('before', {code:'KeyB', isComposing:true, repeat:true, composed:true, key:'old', view:window, detail:7, keyCode:8, charCode:9, which:10});
const result = e.initKeyboardEvent('after');
return result === undefined && e.type === 'after' && e.view === null && e.detail === 0 && e.key === '' &&
e.code === 'KeyB' && e.isComposing && !e.repeat && e.location === 0 && e.keyCode === 8 && e.charCode === 9 && e.which === 10 && e.composed;
});
check('legacy arguments convert left to right before mutation', () => {
const e = new KeyboardEvent('before', {key:'old', location:3});
const old = snapshot(e), seen = [], sentinel = {};
const key = {toString() {seen.push('key'); return 'new';}};
const location = {valueOf() {seen.push('location'); throw sentinel;}};
return throws(() => e.initKeyboardEvent({toString() {seen.push('type'); return 'new';}}, true, true, null, key, location), sentinel) &&
seen.join() === 'type,key,location' && snapshot(e) === old;
});
check('legacy ignored extra arguments are not converted', () => {
const e = new KeyboardEvent('before');
e.initKeyboardEvent('after', false, false, null, '', 0, false, false, false, false, {[Symbol.toPrimitive]() {throw new Error('extra conversion');}});
return e.type === 'after';
});
check('legacy dispatch guard follows conversions and preserves state', () => {
const e = new KeyboardEvent('before', {key:'old', ctrlKey:true, code:'KeyB', composed:true});
const before = snapshot(e), target = new EventTarget();
let converted = false, ignored = false, propagated = false;
target.addEventListener('before', () => {
e.initKeyboardEvent('after', false, false, null, {toString() {converted = true; return 'new';}}, 1, false, true);
ignored = snapshot(e) === before;
propagated = typeError(() => e.initKeyboardEvent('after', false, false, null, Symbol()));
});
return target.dispatchEvent(e) && converted && ignored && propagated && snapshot(e) === before;
});
const real = new KeyboardEvent('before');
const revokedEvent = Proxy.revocable(real, handler); revokedEvent.revoke();
for (const [name, receiver] of [['ordinary', {}], ['prototype', Object.create(KeyboardEvent.prototype)], ['inherit-real', Object.create(real)], ['proxy', new Proxy(real, handler)], ['revoked', revokedEvent.proxy], ['other-interface', new MouseEvent('x')]]) {
check(`legacy rejects ${name} receiver before conversion`, () => {
let converted = false;
const type = {toString() {converted = true; return 'x';}};
return typeError(() => KeyboardEvent.prototype.initKeyboardEvent.call(receiver, type)) && !converted && traps === 0;
});
}
check('cross-realm genuine receiver and Window view', () => {
const e = new other.KeyboardEvent('before');
KeyboardEvent.prototype.initKeyboardEvent.call(e, 'after', true, false, window, texts[4], -1, true);
return e.type === 'after' && e.key === texts[4] && e.view === window && e.location === 4294967295 && e.ctrlKey && e instanceof other.KeyboardEvent;
});
check('cross-realm method TypeError uses callee realm', () => typeError(() => other.KeyboardEvent.prototype.initKeyboardEvent.call({}, 'x'), other));
check('cross-realm constructor TypeError uses callee realm', () => typeError(() => new other.KeyboardEvent('x', {view:{}}), other));
check('cross-realm argument TypeError uses callee realm', () => typeError(() => other.KeyboardEvent.prototype.initKeyboardEvent.call(real, 'x', false, false, null, Symbol()), other));
check('genuine receiver remains valid after prototype replacement', () => {
const e = new KeyboardEvent('before');
Object.setPrototypeOf(e, null);
KeyboardEvent.prototype.initKeyboardEvent.call(e, 'after');
return Object.getOwnPropertyDescriptor(Event.prototype, 'type').get.call(e) === 'after';
});
globalThis.__keyboardEventResults = {rows, observed, total: rows.length, passed: rows.filter(row => row.passed).length};
return rows.every(row => row.passed);
})()
@@ -0,0 +1,16 @@
use super::*;
#[test]
fn keyboard_event_initializers_preserve_dom_strings_and_convert_complete_dictionaries() {
let mut vm = new_storage_html_test_vm("https://keyboard-event-init-webidl.test/");
vm.eval("document.body.innerHTML = '<iframe id=child></iframe>';")
.unwrap();
let result = vm.eval(include_str!("keyboard_event_init.js")).unwrap();
assert_eq!(
result,
"true",
"{}",
vm.eval("JSON.stringify(__keyboardEventResults.rows.filter(row=>!row.passed))")
.unwrap()
);
}
@@ -72,6 +72,7 @@ mod document_domain_lifetime;
mod document_domain_setter;
mod error_event_init;
mod keyboard_event_init;
mod message_event_init;
mod window_current_event_private;