mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
fix(events): preserve keyboard and mouse modifier state
Implement MouseEvent.getModifierState for mouse-derived events and share EventModifierInit parsing with KeyboardEvent. Read a private modifier snapshot with native receiver checks, preserve independent AltGraph and lock states, and reset the snapshot during legacy initialization. Validation: cargo fmt --all; workspace/all-targets/all-features Clippy with warnings denied; 18,303 nextest tests pass, including 6 new regressions. The 47-page WPT comparison improves Moli from 141/158 to 147/158 with no regressions; Chromium remains 158/158.
This commit is contained in:
@@ -176,13 +176,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> {
|
||||
@@ -223,10 +216,6 @@ struct LegacyMouseEventBaseInitDeclaration<'scope> {
|
||||
struct LegacyMouseEventTailInitDeclaration<'scope> {
|
||||
#[webapi(constructor_default = 0)]
|
||||
buttons: i32,
|
||||
ctrl_key: bool,
|
||||
alt_key: bool,
|
||||
shift_key: bool,
|
||||
meta_key: bool,
|
||||
related_target: v8::Local<'scope, v8::Value>,
|
||||
}
|
||||
|
||||
@@ -242,14 +231,6 @@ struct LegacyKeyboardEventInitDeclaration<'scope> {
|
||||
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)]
|
||||
@@ -399,15 +380,17 @@ pub(super) fn mouse_event_init_callback<'s>(
|
||||
)
|
||||
.initialize(scope, event)
|
||||
.expect("legacy MouseEvent base init declaration should initialize");
|
||||
LegacyMouseEventTailInitDeclaration::new(
|
||||
super::events::initialize_legacy_event_modifiers(
|
||||
scope,
|
||||
event,
|
||||
parsed.ctrl_key,
|
||||
parsed.alt_key,
|
||||
parsed.shift_key,
|
||||
parsed.meta_key,
|
||||
related_target,
|
||||
)
|
||||
.initialize(scope, event)
|
||||
.expect("legacy MouseEvent tail init declaration should initialize");
|
||||
);
|
||||
LegacyMouseEventTailInitDeclaration::new(related_target)
|
||||
.initialize(scope, event)
|
||||
.expect("legacy MouseEvent tail init declaration should initialize");
|
||||
}
|
||||
|
||||
pub(super) fn keyboard_event_init_callback<'s>(
|
||||
@@ -432,6 +415,7 @@ pub(super) fn keyboard_event_init_callback<'s>(
|
||||
);
|
||||
let key = v8_string(scope, &parsed.key).unwrap_or_else(|| v8str(scope, ""));
|
||||
let code = v8str(scope, "");
|
||||
super::events::initialize_legacy_event_modifiers(scope, event, false, false, false, false);
|
||||
LegacyKeyboardEventInitDeclaration::new(view, key, code, parsed.location, parsed.repeat)
|
||||
.initialize(scope, event)
|
||||
.expect("legacy KeyboardEvent init declaration should initialize");
|
||||
@@ -517,38 +501,3 @@ pub(super) fn storage_event_init_callback<'s>(
|
||||
parsed.storage_area,
|
||||
);
|
||||
}
|
||||
|
||||
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 = 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,9 +2,8 @@ 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,
|
||||
keyboard_event_init_callback, mouse_event_init_callback, storage_event_init_callback,
|
||||
text_event_init_callback, ui_event_init_callback,
|
||||
},
|
||||
events::{
|
||||
before_unload_event_return_value_getter_function,
|
||||
@@ -18,11 +17,11 @@ use super::{
|
||||
event_cancel_bubble_setter_function, 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_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_event_phase_getter_function, event_get_modifier_state_callback,
|
||||
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,
|
||||
focus_event_related_target_getter_function, form_data_event_form_data_getter_function,
|
||||
mouse_event_related_target_getter_function, pointer_event_get_predicted_events_callback,
|
||||
submit_event_submitter_getter_function, toggle_event_source_getter_function,
|
||||
@@ -288,8 +287,9 @@ struct KeyboardEventTemplateMethodsDeclaration {
|
||||
|
||||
#[webapi(
|
||||
method = "getModifierState",
|
||||
length = 0,
|
||||
callback = keyboard_event_get_modifier_state_callback
|
||||
length = 1,
|
||||
callback = event_get_modifier_state_callback,
|
||||
receiver = web_api_interfaces::KeyboardEvent::is_instance
|
||||
)]
|
||||
get_modifier_state: (),
|
||||
}
|
||||
@@ -297,6 +297,14 @@ struct KeyboardEventTemplateMethodsDeclaration {
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::MouseEvent, enumerable)]
|
||||
struct MouseEventTemplateMethodsDeclaration {
|
||||
#[webapi(
|
||||
method = "getModifierState",
|
||||
length = 1,
|
||||
callback = event_get_modifier_state_callback,
|
||||
receiver = web_api_interfaces::MouseEvent::is_instance
|
||||
)]
|
||||
get_modifier_state: (),
|
||||
|
||||
#[webapi(
|
||||
accessor_property = "relatedTarget",
|
||||
getter = mouse_event_related_target_getter_function
|
||||
|
||||
@@ -6,8 +6,13 @@ mod base;
|
||||
mod init;
|
||||
mod kind;
|
||||
mod methods;
|
||||
mod modifiers;
|
||||
mod subclasses;
|
||||
|
||||
pub(in crate::context_bootstrap) use modifiers::{
|
||||
event_get_modifier_state_callback, initialize_legacy_event_modifiers,
|
||||
};
|
||||
|
||||
const CLOSE_EVENT_WAS_CLEAN_SLOT: &str = "__moliCloseEventWasClean";
|
||||
const CLOSE_EVENT_CODE_SLOT: &str = "__moliCloseEventCode";
|
||||
const CLOSE_EVENT_REASON_SLOT: &str = "__moliCloseEventReason";
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
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")]
|
||||
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 {
|
||||
fn initialize<'s>(&self, scope: &mut v8::PinScope<'s, '_>, event: v8::Local<'s, v8::Object>) {
|
||||
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(super) fn initialize_event_modifiers<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
init: Option<v8::Local<'s, v8::Object>>,
|
||||
) -> bool {
|
||||
let modifiers = match init {
|
||||
Some(init) => {
|
||||
match webidl::parse_dictionary_object::<EventModifierInitMembers>(scope, init) {
|
||||
Ok(modifiers) => modifiers,
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => EventModifierInitMembers::default(),
|
||||
};
|
||||
modifiers.initialize(scope, event);
|
||||
true
|
||||
}
|
||||
|
||||
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 = get_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());
|
||||
}
|
||||
@@ -14,10 +14,6 @@ struct KeyboardEventInitDeclaration<'scope> {
|
||||
which: f64,
|
||||
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>(
|
||||
@@ -29,6 +25,9 @@ pub(in crate::context_bootstrap::events::subclasses) fn initialize_keyboard_even
|
||||
return false;
|
||||
};
|
||||
let detail = init_number_property(scope, init, "detail", 0.0);
|
||||
if !super::super::modifiers::initialize_event_modifiers(scope, event, init) {
|
||||
return false;
|
||||
}
|
||||
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");
|
||||
@@ -45,10 +44,6 @@ pub(in crate::context_bootstrap::events::subclasses) fn initialize_keyboard_even
|
||||
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),
|
||||
)
|
||||
.initialize(scope, event)
|
||||
.expect("KeyboardEvent init declaration should initialize");
|
||||
|
||||
@@ -47,15 +47,6 @@ struct MouseEventBaseInitDeclaration<'scope> {
|
||||
movement_y: f64,
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(plain, data_properties, enumerable)]
|
||||
struct MouseEventModifierInitDeclaration {
|
||||
ctrl_key: bool,
|
||||
shift_key: bool,
|
||||
alt_key: bool,
|
||||
meta_key: bool,
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(plain, data_properties, enumerable)]
|
||||
struct MouseEventRelatedTargetDeclaration<'scope> {
|
||||
@@ -389,9 +380,12 @@ fn define_mouse_event_base_fields<'s>(
|
||||
let Ok(view) = init_window_view_property(scope, init, constructor_name) else {
|
||||
return None;
|
||||
};
|
||||
let detail = init_number_property(scope, init, "detail", 0.0);
|
||||
if !super::super::modifiers::initialize_event_modifiers(scope, event, init) {
|
||||
return None;
|
||||
}
|
||||
let client_x = init_number_property(scope, init, "clientX", 0.0);
|
||||
let client_y = init_number_property(scope, init, "clientY", 0.0);
|
||||
let detail = init_number_property(scope, init, "detail", 0.0);
|
||||
let related_target =
|
||||
init_value_property(scope, init, "relatedTarget").unwrap_or_else(|| v8::null(scope).into());
|
||||
|
||||
@@ -416,21 +410,6 @@ fn define_mouse_event_base_fields<'s>(
|
||||
Some(related_target)
|
||||
}
|
||||
|
||||
fn define_mouse_event_modifier_fields<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
init: Option<v8::Local<'s, v8::Object>>,
|
||||
) {
|
||||
MouseEventModifierInitDeclaration::new(
|
||||
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),
|
||||
)
|
||||
.initialize(scope, event)
|
||||
.expect("MouseEvent modifier init declaration should initialize");
|
||||
}
|
||||
|
||||
fn define_mouse_event_related_target<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
@@ -450,7 +429,6 @@ pub(in crate::context_bootstrap::events::subclasses) fn initialize_mouse_event<'
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
define_mouse_event_modifier_fields(scope, event, init);
|
||||
define_mouse_event_related_target(scope, event, related_target);
|
||||
true
|
||||
}
|
||||
@@ -472,7 +450,6 @@ pub(in crate::context_bootstrap::events::subclasses) fn initialize_wheel_event<'
|
||||
)
|
||||
.initialize(scope, event)
|
||||
.expect("WheelEvent delta init declaration should initialize");
|
||||
define_mouse_event_modifier_fields(scope, event, init);
|
||||
define_mouse_event_related_target(scope, event, related_target);
|
||||
true
|
||||
}
|
||||
@@ -523,7 +500,6 @@ pub(in crate::context_bootstrap::events::subclasses) fn initialize_pointer_event
|
||||
)
|
||||
.initialize(scope, event)
|
||||
.expect("PointerEvent number init declaration should initialize");
|
||||
define_mouse_event_modifier_fields(scope, event, init);
|
||||
PointerEventTailInitDeclaration::new(
|
||||
init_bool_property(scope, init, "isPrimary", false),
|
||||
pointer_type,
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn event_modifier_state_constructor_keeps_each_modifier_independent() {
|
||||
let mut vm = new_storage_test_vm("https://event-modifiers.test/");
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const modifiers = [
|
||||
['Control', 'ctrlKey'], ['Shift', 'shiftKey'], ['Alt', 'altKey'], ['Meta', 'metaKey'],
|
||||
...['AltGraph', 'CapsLock', 'Fn', 'FnLock', 'Hyper', 'NumLock', 'ScrollLock',
|
||||
'Super', 'Symbol', 'SymbolLock'].map(name => [name, 'modifier' + name])
|
||||
];
|
||||
for (const Ctor of [KeyboardEvent, MouseEvent, WheelEvent, PointerEvent, DragEvent]) {
|
||||
const empty = new Ctor('event');
|
||||
for (const [key, member] of modifiers) {
|
||||
if (empty.getModifierState(key)) return Ctor.name + ': default ' + key;
|
||||
const event = new Ctor('event', {[member]: true});
|
||||
for (const [queried] of modifiers) {
|
||||
if (event.getModifierState(queried) !== (queried === key)) {
|
||||
return Ctor.name + ': ' + key + ' changed ' + queried;
|
||||
}
|
||||
}
|
||||
for (const value of [false, 0, '', null, undefined]) {
|
||||
if (new Ctor('event', {[member]: value}).getModifierState(key)) {
|
||||
return Ctor.name + ': boolean conversion ' + key;
|
||||
}
|
||||
}
|
||||
const truthy = {valueOf() { throw new Error('boolean conversion called valueOf'); }};
|
||||
if (!new Ctor('event', {[member]: truthy}).getModifierState(key)) return 'truthy ' + key;
|
||||
if (member.startsWith('modifier') && member in event) return 'exposed init member ' + member;
|
||||
}
|
||||
for (const key of ['control', 'Unknown', '', 'Alt\0Graph']) {
|
||||
if (empty.getModifierState(key)) return 'unknown modifier ' + key;
|
||||
}
|
||||
}
|
||||
return 'ok';
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("event modifier constructor probe should run");
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_modifier_state_ignores_shadow_properties_and_checks_receivers() {
|
||||
let mut vm = new_storage_test_vm("https://event-modifiers.test/");
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
for (const Ctor of [KeyboardEvent, MouseEvent, WheelEvent, PointerEvent, DragEvent]) {
|
||||
const event = new Ctor('event', {ctrlKey: true, modifierAltGraph: true});
|
||||
const method = event.getModifierState;
|
||||
Object.defineProperty(event, 'ctrlKey', {get() {throw Error('shadow property'); }});
|
||||
if (!method.call(event, 'Control') || !method.call(event, 'AltGraph')) return 'shadow ' + Ctor.name;
|
||||
Object.setPrototypeOf(event, null);
|
||||
if (!method.call(event, 'Control')) return 'changed prototype ' + Ctor.name;
|
||||
for (const receiver of [{ctrlKey: true}, Object.create(Ctor.prototype), new Proxy(event, {}), new Event('event')]) {
|
||||
let converted = false;
|
||||
const key = {toString() {converted = true; return 'Control'; }};
|
||||
try {method.call(receiver, key); return 'accepted receiver ' + Ctor.name;}
|
||||
catch (error) {if (!(error instanceof TypeError) || converted) return 'receiver order ' + Ctor.name;}
|
||||
}
|
||||
}
|
||||
for (const [method, receiver] of [
|
||||
[KeyboardEvent.prototype.getModifierState, new MouseEvent('event')],
|
||||
[MouseEvent.prototype.getModifierState, new KeyboardEvent('event')],
|
||||
]) {
|
||||
try {method.call(receiver, 'Control'); return 'accepted unrelated interface';}
|
||||
catch (error) {if (!(error instanceof TypeError)) return 'wrong receiver error';}
|
||||
}
|
||||
for (const Ctor of [MouseEvent, KeyboardEvent]) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Ctor.prototype, 'getModifierState');
|
||||
if (!descriptor.writable || !descriptor.enumerable || !descriptor.configurable || descriptor.value.length !== 1) {
|
||||
return 'method descriptor ' + Ctor.name;
|
||||
}
|
||||
}
|
||||
return 'ok';
|
||||
})()
|
||||
"#).expect("event modifier receiver probe should run");
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_modifier_state_arguments_preserve_webidl_conversion_errors() {
|
||||
let mut vm = new_storage_test_vm("https://event-modifiers.test/");
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
for (const Ctor of [KeyboardEvent, MouseEvent, WheelEvent, PointerEvent, DragEvent]) {
|
||||
const event = new Ctor('event', {ctrlKey: true});
|
||||
for (const args of [[], [Symbol('Control')]]) {
|
||||
try {event.getModifierState(...args); return 'accepted argument ' + Ctor.name;}
|
||||
catch (error) {if (!(error instanceof TypeError)) return 'wrong argument error';}
|
||||
}
|
||||
if (event.getModifierState(undefined) || event.getModifierState(null)) return 'nullable conversion';
|
||||
let reads = 0;
|
||||
if (!event.getModifierState({toString() {reads++; return 'Control'; }}) || reads !== 1) return 'string conversion';
|
||||
const sentinel = new RangeError('key conversion');
|
||||
try {event.getModifierState({toString() {throw sentinel; }}); return 'swallowed argument exception';}
|
||||
catch (error) {if (error !== sentinel) return 'replaced argument exception';}
|
||||
}
|
||||
return 'ok';
|
||||
})()
|
||||
"#).expect("event modifier argument probe should run");
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_modifier_state_dictionary_reads_once_and_propagates_getter_exceptions() {
|
||||
let mut vm = new_storage_test_vm("https://event-modifiers.test/");
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
const members = ['ctrlKey', 'altKey', 'metaKey', 'shiftKey',
|
||||
...['AltGraph', 'CapsLock', 'Fn', 'FnLock', 'Hyper', 'NumLock', 'ScrollLock', 'Super', 'Symbol', 'SymbolLock'].map(name => 'modifier' + name)
|
||||
].sort();
|
||||
for (const Ctor of [KeyboardEvent, MouseEvent, WheelEvent, PointerEvent, DragEvent]) {
|
||||
const reads = [];
|
||||
const init = new Proxy({}, {get(target, key) {
|
||||
if (members.includes(key)) {reads.push(key); return true;}
|
||||
}});
|
||||
const event = new Ctor('event', init);
|
||||
if (reads.join() !== members.join()) return Ctor.name + ': read order ' + reads;
|
||||
if (!event.getModifierState('AltGraph') || reads.length !== members.length) return 'state rereads init';
|
||||
const sentinel = new RangeError('modifier getter');
|
||||
let laterRead = false;
|
||||
try {
|
||||
new Ctor('event', {
|
||||
get modifierCapsLock() {throw sentinel;},
|
||||
get modifierFn() {laterRead = true; return true;},
|
||||
});
|
||||
return 'swallowed getter exception ' + Ctor.name;
|
||||
} catch (error) {
|
||||
if (error !== sentinel || laterRead) return 'getter exception order ' + Ctor.name;
|
||||
}
|
||||
}
|
||||
return 'ok';
|
||||
})()
|
||||
"#).expect("event modifier dictionary probe should run");
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_modifier_state_legacy_initializers_reset_only_when_allowed() {
|
||||
let mut vm = new_storage_test_vm("https://event-modifiers.test/");
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
const init = {ctrlKey: true, modifierAltGraph: true, modifierCapsLock: true};
|
||||
for (const Ctor of [MouseEvent, WheelEvent, PointerEvent, DragEvent]) {
|
||||
const event = new Ctor('event', init);
|
||||
event.initEvent('again');
|
||||
if (!event.getModifierState('Control') || !event.getModifierState('AltGraph')) return 'initEvent reset ' + Ctor.name;
|
||||
const target = document.createElement('div');
|
||||
target.addEventListener('again', () => event.initMouseEvent('ignored'));
|
||||
target.dispatchEvent(event);
|
||||
if (event.type !== 'again' || !event.getModifierState('Control')) return 'reset during dispatch';
|
||||
event.initMouseEvent('changed', false, false, null, 0, 0, 0, 0, 0, false, true, true, false);
|
||||
if (event.getModifierState('Control') || event.getModifierState('AltGraph') || event.getModifierState('CapsLock')) return 'retained old modifier';
|
||||
if (!event.getModifierState('Alt') || !event.getModifierState('Shift')) return 'legacy modifier arguments';
|
||||
event.initMouseEvent('empty');
|
||||
if (event.getModifierState('Alt') || event.getModifierState('Shift')) return 'legacy default modifiers';
|
||||
}
|
||||
const keyboard = new KeyboardEvent('event', init);
|
||||
keyboard.initKeyboardEvent('empty');
|
||||
if (keyboard.getModifierState('Control') || keyboard.getModifierState('AltGraph') || keyboard.getModifierState('CapsLock')) return 'legacy keyboard reset';
|
||||
const created = document.createEvent('MouseEvents');
|
||||
created.initMouseEvent('click', false, false, null, 0, 0, 0, 0, 0, true);
|
||||
if (!created.getModifierState('Control')) return 'createEvent modifiers';
|
||||
return 'ok';
|
||||
})()
|
||||
"#).expect("event modifier legacy initializer probe should run");
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_modifier_state_accepts_cross_realm_native_receivers() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://event-modifiers.test/",
|
||||
"<!doctype html><html><body></body></html>",
|
||||
);
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.appendChild(frame);
|
||||
const child = frame.contentWindow;
|
||||
for (const name of ['MouseEvent', 'KeyboardEvent']) {
|
||||
const local = new window[name]('event', {shiftKey: true});
|
||||
const foreign = new child[name]('event', {ctrlKey: true});
|
||||
const localMethod = window[name].prototype.getModifierState;
|
||||
const foreignMethod = child[name].prototype.getModifierState;
|
||||
if (!localMethod.call(foreign, 'Control') || !foreignMethod.call(local, 'Shift')) return 'cross-realm ' + name;
|
||||
try {foreignMethod.call({}, 'Control'); return 'accepted forged receiver';}
|
||||
catch (error) {if (!(error instanceof child.TypeError)) return 'wrong exception realm';}
|
||||
}
|
||||
frame.remove();
|
||||
return 'ok';
|
||||
})()
|
||||
"#).expect("cross-realm event modifier probe should run");
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
@@ -7,6 +7,7 @@ mod cors_response;
|
||||
mod cssom;
|
||||
mod data_response;
|
||||
mod dom;
|
||||
mod event_modifiers;
|
||||
mod file_input;
|
||||
mod forms;
|
||||
mod misc;
|
||||
|
||||
Reference in New Issue
Block a user