mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(dom): restore legacy createEvent interface semantics
This commit is contained in:
@@ -38,7 +38,7 @@ enum DocumentCreateEventKind {
|
||||
KeyboardEvent,
|
||||
MessageEvent,
|
||||
StorageEvent,
|
||||
SubmitEvent,
|
||||
TouchEvent,
|
||||
}
|
||||
|
||||
impl DocumentCreateEventKind {
|
||||
@@ -49,17 +49,17 @@ impl DocumentCreateEventKind {
|
||||
DocumentCreateEventKind::CustomEvent => "CustomEvent",
|
||||
DocumentCreateEventKind::DeviceMotionEvent => "DeviceMotionEvent",
|
||||
DocumentCreateEventKind::DeviceOrientationEvent => "DeviceOrientationEvent",
|
||||
DocumentCreateEventKind::DragEvent => "Event",
|
||||
DocumentCreateEventKind::DragEvent => "DragEvent",
|
||||
DocumentCreateEventKind::UiEvent => "UIEvent",
|
||||
DocumentCreateEventKind::TextEvent => "TextEvent",
|
||||
DocumentCreateEventKind::CompositionEvent => "CompositionEvent",
|
||||
DocumentCreateEventKind::FocusEvent => "FocusEvent",
|
||||
DocumentCreateEventKind::HashChangeEvent => "Event",
|
||||
DocumentCreateEventKind::HashChangeEvent => "HashChangeEvent",
|
||||
DocumentCreateEventKind::MouseEvent => "MouseEvent",
|
||||
DocumentCreateEventKind::KeyboardEvent => "KeyboardEvent",
|
||||
DocumentCreateEventKind::MessageEvent => "MessageEvent",
|
||||
DocumentCreateEventKind::StorageEvent => "StorageEvent",
|
||||
DocumentCreateEventKind::SubmitEvent => "SubmitEvent",
|
||||
DocumentCreateEventKind::TouchEvent => "TouchEvent",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,11 +176,14 @@ mod tests {
|
||||
.map(DocumentCreateEventKind::constructor_name),
|
||||
Ok("MouseEvent")
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentCreateEventKind::from_str("SubmitEvent")
|
||||
.map(DocumentCreateEventKind::constructor_name),
|
||||
Ok("SubmitEvent")
|
||||
);
|
||||
for name in ["DragEvent", "HashChangeEvent", "TouchEvent"] {
|
||||
assert_eq!(
|
||||
DocumentCreateEventKind::from_str(name)
|
||||
.map(DocumentCreateEventKind::constructor_name),
|
||||
Ok(name)
|
||||
);
|
||||
}
|
||||
assert!(DocumentCreateEventKind::from_str("SubmitEvent").is_err());
|
||||
assert!(DocumentCreateEventKind::from_str("PointerEvent").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +224,15 @@ struct ToggleEventTemplateAccessorsDeclaration {
|
||||
source: (),
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::HashChangeEvent, enumerable, receiver)]
|
||||
struct HashChangeEventTemplateAccessorsDeclaration {
|
||||
#[webapi(accessor_property = "oldURL", getter = event_value_attribute_getter, data = crate::util::v8str(scope, "oldURL"))]
|
||||
old_url: (),
|
||||
#[webapi(accessor_property = "newURL", getter = event_value_attribute_getter, data = crate::util::v8str(scope, "newURL"))]
|
||||
new_url: (),
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::UIEvent, enumerable, receiver)]
|
||||
struct UiEventTemplateMethodsDeclaration {
|
||||
@@ -673,6 +682,9 @@ fn event_template_declaration(interface: &str) -> Option<EventTemplateDeclaratio
|
||||
EventTemplateDeclaration::new::<CommandEventTemplateAccessorsDeclaration>()
|
||||
}
|
||||
"ToggleEvent" => EventTemplateDeclaration::new::<ToggleEventTemplateAccessorsDeclaration>(),
|
||||
"HashChangeEvent" => {
|
||||
EventTemplateDeclaration::new::<HashChangeEventTemplateAccessorsDeclaration>()
|
||||
}
|
||||
"EventTarget" => EventTemplateDeclaration::new::<EventTargetTemplateMethodsDeclaration>(),
|
||||
"Document" => EventTemplateDeclaration::new::<DocumentEventTemplateMethodsDeclaration>(),
|
||||
_ => return None,
|
||||
|
||||
@@ -290,8 +290,8 @@ fn initialize_touch_ui_event<'s>(
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
init: Option<v8::Local<'s, v8::Object>>,
|
||||
) {
|
||||
let view = touch_event_init_value(scope, init, "view")
|
||||
.unwrap_or_else(|| scope.get_current_context().global(scope).into());
|
||||
let view =
|
||||
touch_event_init_value(scope, init, "view").unwrap_or_else(|| v8::null(scope).into());
|
||||
let detail = init
|
||||
.and_then(|object| object.get(scope, v8str(scope, "detail").into()))
|
||||
.filter(|value| !value.is_null_or_undefined())
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
(() => {
|
||||
const checks = [];
|
||||
const check = (name, fn) => {
|
||||
try { checks.push({name, passed: fn() === true}); }
|
||||
catch (error) { checks.push({name, passed: false, error: String(error)}); }
|
||||
};
|
||||
const realms = [window, document.querySelector('iframe').contentWindow];
|
||||
const aliases = [
|
||||
['BeforeUnloadEvent', 'BeforeUnloadEvent'], ['CompositionEvent', 'CompositionEvent'],
|
||||
['CustomEvent', 'CustomEvent'], ['DeviceMotionEvent', 'DeviceMotionEvent'],
|
||||
['DeviceOrientationEvent', 'DeviceOrientationEvent'], ['DragEvent', 'DragEvent'],
|
||||
['Event', 'Event'], ['Events', 'Event'], ['FocusEvent', 'FocusEvent'],
|
||||
['HashChangeEvent', 'HashChangeEvent'], ['HTMLEvents', 'Event'],
|
||||
['KeyboardEvent', 'KeyboardEvent'], ['MessageEvent', 'MessageEvent'],
|
||||
['MouseEvent', 'MouseEvent'], ['MouseEvents', 'MouseEvent'],
|
||||
['StorageEvent', 'StorageEvent'], ['SVGEvents', 'Event'], ['TextEvent', 'TextEvent'],
|
||||
['TouchEvent', 'TouchEvent'], ['UIEvent', 'UIEvent'], ['UIEvents', 'UIEvent']
|
||||
];
|
||||
const rejected = (realm, operation, name) => {
|
||||
try { operation(); }
|
||||
catch (error) {
|
||||
return Object.getPrototypeOf(error) === realm.DOMException.prototype &&
|
||||
error.name === name;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const typeError = (realm, operation) => {
|
||||
try { operation(); }
|
||||
catch (error) { return Object.getPrototypeOf(error) === realm.TypeError.prototype; }
|
||||
return false;
|
||||
};
|
||||
const defaults = event => event.type === '' && event.target === null &&
|
||||
event.currentTarget === null && event.eventPhase === 0 && !event.bubbles &&
|
||||
!event.cancelable && !event.composed && !event.defaultPrevented &&
|
||||
!event.isTrusted && Number.isFinite(event.timeStamp);
|
||||
for (const [ownerIndex, owner] of realms.entries()) {
|
||||
const docs = [owner.document, owner.document.implementation.createHTMLDocument(''),
|
||||
owner.document.implementation.createDocument('urn:legacy-event', 'root'),
|
||||
new owner.DOMParser().parseFromString('<root/>', 'application/xml')];
|
||||
for (const [docIndex, doc] of docs.entries()) {
|
||||
for (const [calleeIndex, callee] of realms.entries()) {
|
||||
const prefix = `${ownerIndex}/${docIndex}/${calleeIndex}/`;
|
||||
const factory = callee.Document.prototype.createEvent;
|
||||
const create = name => factory.call(doc, name);
|
||||
const exposed = name => typeof owner[name] === 'function' &&
|
||||
(name !== 'TouchEvent' || 'ontouchstart' in doc);
|
||||
check(prefix + 'factory metadata', () => {
|
||||
const d = Object.getOwnPropertyDescriptor(callee.Document.prototype, 'createEvent');
|
||||
return d.enumerable && d.configurable && d.writable &&
|
||||
factory.name === 'createEvent' && factory.length === 1;
|
||||
});
|
||||
for (const [alias, name] of aliases) {
|
||||
for (const variant of [alias, alias.toLowerCase(), alias.toUpperCase()]) {
|
||||
check(prefix + 'alias/defaults/' + variant, () => {
|
||||
if (!exposed(name)) return rejected(callee, () => create(variant), 'NotSupportedError');
|
||||
const event = create(variant);
|
||||
return Object.getPrototypeOf(event) === owner[name].prototype && defaults(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const name of ['DragEvent', 'HashChangeEvent', 'TouchEvent']) {
|
||||
const unavailable = () => rejected(callee, () => create(name), 'NotSupportedError');
|
||||
check(prefix + name + '/subclass state', () => {
|
||||
if (!exposed(name)) return unavailable();
|
||||
const event = create(name);
|
||||
if (name === 'HashChangeEvent') return event.oldURL === '' && event.newURL === '';
|
||||
if (name === 'DragEvent') return event.dataTransfer === null && event.view === null &&
|
||||
event.detail === 0 && event.clientX === 0 && event.clientY === 0 && !event.ctrlKey;
|
||||
return event.touches.length === 0 && event.targetTouches.length === 0 &&
|
||||
event.changedTouches.length === 0 && event.view === null && event.detail === 0;
|
||||
});
|
||||
check(prefix + name + '/native brand', () => {
|
||||
if (!exposed(name)) return unavailable();
|
||||
const event = create(name), key = name === 'HashChangeEvent' ? 'oldURL' :
|
||||
name === 'DragEvent' ? 'dataTransfer' : 'touches';
|
||||
const getter = Object.getOwnPropertyDescriptor(callee[name].prototype, key).get;
|
||||
const value = getter.call(event);
|
||||
return name === 'HashChangeEvent' ? value === '' :
|
||||
name === 'DragEvent' ? value === null : value.length === 0;
|
||||
});
|
||||
check(prefix + name + '/uninitialized dispatch rejected', () => {
|
||||
if (!exposed(name)) return unavailable();
|
||||
return rejected(owner, () => new owner.EventTarget().dispatchEvent(create(name)), 'InvalidStateError');
|
||||
});
|
||||
check(prefix + name + '/initialize and dispatch', () => {
|
||||
if (!exposed(name)) return unavailable();
|
||||
const event = create(name), target = new owner.EventTarget();
|
||||
let seen = 0, state = false;
|
||||
target.addEventListener('legacy', current => {
|
||||
seen++;
|
||||
state = current === event && current.target === target &&
|
||||
current.currentTarget === target && current.eventPhase === 2;
|
||||
current.preventDefault();
|
||||
});
|
||||
const initialized = callee.Event.prototype.initEvent.call(event, 'legacy', false, true);
|
||||
return initialized === undefined && target.dispatchEvent(event) === false &&
|
||||
seen === 1 && state && event.defaultPrevented && event.currentTarget === null &&
|
||||
Object.getPrototypeOf(event) === owner[name].prototype;
|
||||
});
|
||||
check(prefix + name + '/reinitialize retains subclass', () => {
|
||||
if (!exposed(name)) return unavailable();
|
||||
const event = create(name), target = new owner.EventTarget();
|
||||
event.initEvent('first', true, true);
|
||||
target.addEventListener('first', current => current.initEvent('during', false, false));
|
||||
target.dispatchEvent(event);
|
||||
if (event.type !== 'first' || !event.bubbles || !event.cancelable) return false;
|
||||
event.initEvent('second', false, false);
|
||||
return event.type === 'second' && event.target === null && !event.bubbles &&
|
||||
!event.cancelable && !event.defaultPrevented && Object.getPrototypeOf(event) === owner[name].prototype;
|
||||
});
|
||||
}
|
||||
for (const name of ['', 'TimeEvent', 'SubmitEvent', 'PointerEvent', 'WheelEvent',
|
||||
'SVGEvent', 'DragEvents', 'HashChangeEvents', 'TouchEvents', 'UIEvent ', ' UIEvent',
|
||||
'U\u0130Event', 'U\u0131Event', 'ErrorEvent', 'FormDataEvent', 'AnimationEvent']) {
|
||||
check(prefix + 'unsupported/' + name, () => rejected(callee, () => create(name), 'NotSupportedError'));
|
||||
}
|
||||
check(prefix + 'missing argument', () => typeError(callee, () => factory.call(doc)));
|
||||
check(prefix + 'conversion once', () => {
|
||||
let calls = 0;
|
||||
const event = create({toString() { calls++; return 'HashChangeEvent'; }});
|
||||
return calls === 1 && Object.getPrototypeOf(event) === owner.HashChangeEvent.prototype;
|
||||
});
|
||||
let traps = 0;
|
||||
const proxy = new owner.Proxy(doc, {get() { traps++; throw 42; }, getPrototypeOf() { traps++; throw 42; }});
|
||||
const revoked = owner.Proxy.revocable(doc, {}); revoked.revoke();
|
||||
for (const [index, receiver] of [{}, Object.create(doc), Object.create(callee.Document.prototype),
|
||||
proxy, revoked.proxy, doc.createElement('div')].entries()) {
|
||||
check(prefix + 'receiver/' + index, () => {
|
||||
let conversions = 0;
|
||||
return typeError(callee, () => factory.call(receiver, {toString() { conversions++; throw 42; }})) &&
|
||||
conversions === 0 && traps === 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
globalThis.__documentCreateEventResults = {complete: true, total: checks.length,
|
||||
passed: checks.filter(row => row.passed).length, checks};
|
||||
globalThis.__uiEventResults = globalThis.__documentCreateEventResults;
|
||||
return true;
|
||||
})()
|
||||
@@ -0,0 +1,110 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hash_change_event_payload_accessors_use_native_state_and_callee_receiver_checks() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://hash-change-event-accessors.test/",
|
||||
"<!doctype html><body><iframe></iframe></body>",
|
||||
);
|
||||
assert_eq!(vm.eval(r#"(() => {
|
||||
const realms=[window,document.querySelector('iframe').contentWindow];
|
||||
for (const owner of realms) for (const callee of realms) {
|
||||
const event=new owner.HashChangeEvent('hashchange',{
|
||||
oldURL:'https://example.test/#old',newURL:'https://example.test/#new'
|
||||
});
|
||||
event.initEvent('initialized',false,false);
|
||||
for (const [key,value] of [['oldURL','https://example.test/#old'],['newURL','https://example.test/#new']]) {
|
||||
const descriptor=Object.getOwnPropertyDescriptor(callee.HashChangeEvent.prototype,key);
|
||||
const getter=descriptor.get;
|
||||
if (!descriptor.enumerable || !descriptor.configurable || descriptor.set!==undefined ||
|
||||
getter.length!==0 || getter.name!=='get '+key || Object.hasOwn(event,key) || getter.call(event)!==value) throw Error('payload descriptor');
|
||||
Object.defineProperty(event,key,{value:'author value',configurable:true});
|
||||
if (getter.call(event)!==value) throw Error('author field replaced native state');
|
||||
let traps=0;
|
||||
const author=new Proxy(event,{get(){traps++;throw 42;},getPrototypeOf(){traps++;throw 42;}});
|
||||
const revoked=Proxy.revocable(event,{});revoked.revoke();
|
||||
for (const receiver of [{},new owner.Event('event'),Object.create(event),author,revoked.proxy]) {
|
||||
let error;
|
||||
try { getter.call(receiver); } catch(e) { error=e; }
|
||||
if (!error || Object.getPrototypeOf(error)!==callee.TypeError.prototype || traps) throw Error('receiver accepted');
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})()"#).unwrap(), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_create_event_preserves_legacy_interfaces_and_native_dispatch() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://document-create-event.test/",
|
||||
"<!doctype html><body><iframe></iframe></body>",
|
||||
);
|
||||
vm.eval(include_str!("document_create_event.js")).unwrap();
|
||||
assert_eq!(
|
||||
vm.eval("__documentCreateEventResults.complete").unwrap(),
|
||||
"true"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("__documentCreateEventResults.total").unwrap(),
|
||||
"1648"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(__documentCreateEventResults.checks.filter(row => !row.passed))")
|
||||
.unwrap(),
|
||||
"[]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_create_event_uses_intrinsics_and_registered_native_receivers() {
|
||||
let mut vm =
|
||||
new_storage_page_task_executor_test_vm("https://document-create-event-native.test/");
|
||||
vm.eval("document.body.innerHTML='<iframe></iframe>'; globalThis.child=document.querySelector('iframe').contentWindow;")
|
||||
.unwrap();
|
||||
let context_ptr = &vm.page_default_context as *const _;
|
||||
vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let key = crate::util::v8str(scope, "document");
|
||||
let document =
|
||||
v8::Local::<v8::Object>::try_from(global.get(scope, key.into()).unwrap()).unwrap();
|
||||
let handler = crate::util::new_null_prototype_object(scope);
|
||||
let proxy = v8::Proxy::new(scope, document, handler).unwrap();
|
||||
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
|
||||
let key = crate::util::v8str(scope, "nativeDocument");
|
||||
assert_eq!(
|
||||
global.create_data_property(scope, key.into(), proxy.into()),
|
||||
Some(true)
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval(r#"(() => {
|
||||
const names=['DragEvent','HashChangeEvent','TouchEvent'];
|
||||
const originals=names.map(name=>window[name]);
|
||||
let constructorCalls=0;
|
||||
try {
|
||||
for (const name of names) window[name]=function(){constructorCalls++;throw 42;};
|
||||
for (const realm of [window,child]) {
|
||||
const create=realm.Document.prototype.createEvent;
|
||||
for (const [index,name] of names.entries()) {
|
||||
const event=create.call(nativeDocument,name);
|
||||
if (Object.getPrototypeOf(event)!==originals[index].prototype || event.type!=='' || event.isTrusted) throw Error('intrinsic creation');
|
||||
event.initEvent('native',false,false);
|
||||
if (!new EventTarget().dispatchEvent(event)) throw Error('native dispatch');
|
||||
}
|
||||
let conversions=0, traps=0;
|
||||
const author=new Proxy(nativeDocument,{get(){traps++;throw 42;},getPrototypeOf(){traps++;throw 42;}});
|
||||
const revoked=Proxy.revocable(nativeDocument,{});revoked.revoke();
|
||||
for (const receiver of [author,revoked.proxy,Object.create(nativeDocument)]) {
|
||||
let error;
|
||||
try { create.call(receiver,{toString(){conversions++;throw 42;}}); } catch(e) { error=e; }
|
||||
if (!error || Object.getPrototypeOf(error)!==realm.TypeError.prototype || conversions || traps) throw Error('author receiver accepted');
|
||||
}
|
||||
}
|
||||
return constructorCalls===0;
|
||||
} finally {
|
||||
names.forEach((name,index)=>window[name]=originals[index]);
|
||||
}
|
||||
})()"#).unwrap(), "true");
|
||||
}
|
||||
@@ -3129,7 +3129,8 @@ fn submit_event_constructor_surface() {
|
||||
submitter: button
|
||||
});
|
||||
const desc = Object.getOwnPropertyDescriptor(SubmitEvent.prototype, 'submitter');
|
||||
const created = document.createEvent('SubmitEvent');
|
||||
let creationError;
|
||||
try { document.createEvent('SubmitEvent'); } catch (error) { creationError = error; }
|
||||
return [
|
||||
typeof SubmitEvent,
|
||||
SubmitEvent.length,
|
||||
@@ -3146,8 +3147,8 @@ fn submit_event_constructor_surface() {
|
||||
Object.prototype.toString.call(second),
|
||||
typeof desc.get,
|
||||
desc.set === undefined,
|
||||
created instanceof SubmitEvent,
|
||||
created.submitter === null
|
||||
creationError instanceof DOMException,
|
||||
creationError.name === 'NotSupportedError'
|
||||
].join('|');
|
||||
})()
|
||||
"#,
|
||||
|
||||
@@ -2245,6 +2245,7 @@ mod device_events;
|
||||
|
||||
mod gamepad_interfaces;
|
||||
|
||||
mod document_create_event;
|
||||
mod document_svg_root;
|
||||
mod dom_elements;
|
||||
mod dom_exception_proxy_identity;
|
||||
|
||||
@@ -1582,7 +1582,7 @@ wait_until = "load"
|
||||
timeout_ms = 5000
|
||||
suite = "smoke"
|
||||
tags = ["forms", "html", "submit", "events"]
|
||||
notes = "Manual compat port for SubmitEvent surface: constructor/getter shape, document.createEvent('SubmitEvent'), requestSubmit(submitter) preserving the active submitter, and requestSubmit() exposing null submitter."
|
||||
notes = "Manual compat port for SubmitEvent surface: constructor/getter shape, legacy document.createEvent('SubmitEvent') rejection, requestSubmit(submitter) preserving the active submitter, and requestSubmit() exposing null submitter."
|
||||
|
||||
[[test]]
|
||||
id = "form-submit-button-click-basic"
|
||||
|
||||
@@ -16,7 +16,6 @@ test(function () {
|
||||
submitter: button,
|
||||
});
|
||||
const descriptor = Object.getOwnPropertyDescriptor(SubmitEvent.prototype, "submitter");
|
||||
const created = document.createEvent("SubmitEvent");
|
||||
|
||||
assert_equals(typeof SubmitEvent, "function", "SubmitEvent should be exposed");
|
||||
assert_equals(SubmitEvent.length, 1, "SubmitEvent.length should match the constructor signature");
|
||||
@@ -45,8 +44,9 @@ test(function () {
|
||||
assert_equals(typeof descriptor.get, "function", "submitter should be a getter");
|
||||
assert_equals(descriptor.set, undefined, "submitter should not expose a setter");
|
||||
|
||||
assert_true(created instanceof SubmitEvent, "document.createEvent('SubmitEvent') should use SubmitEvent");
|
||||
assert_equals(created.submitter, null, "document.createEvent('SubmitEvent') should default submitter to null");
|
||||
assert_throws_dom("NotSupportedError", function () {
|
||||
document.createEvent("SubmitEvent");
|
||||
}, "SubmitEvent is absent from the legacy createEvent interface table");
|
||||
}, "SubmitEvent constructor and prototype getter surface");
|
||||
|
||||
test(function () {
|
||||
|
||||
Reference in New Issue
Block a user