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:
ldm0
2026-09-14 20:16:29 +08:00
parent b3190a8dde
commit 5d0176da69
8 changed files with 407 additions and 105 deletions
@@ -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;