Skip to content

Commit 66fb059

Browse files
daeyeontargos
authored andcommitted
events: improve EventListener validation
This fixes validating `EventListener` given to `add/removeEventListener` to improve the Web API compatibility. According to the WPT test failure with the current validation, `addEventListener` should not require `handleEvent` to be defined on an `EventListener`". IIUC, the same can be applied to `removeEventListener` also. Signed-off-by: Daeyeon Jeong daeyeon.dev@gmail.com PR-URL: #43491 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent ad1d054 commit 66fb059

File tree

3 files changed

+156
-7
lines changed

3 files changed

+156
-7
lines changed

lib/internal/event_target.js

+13-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const {
44
ArrayFrom,
55
Boolean,
66
Error,
7-
FunctionPrototypeBind,
87
FunctionPrototypeCall,
98
NumberIsInteger,
109
ObjectAssign,
@@ -381,7 +380,10 @@ class Listener {
381380
this.callback = listener;
382381
this.listener = listener;
383382
} else {
384-
this.callback = FunctionPrototypeBind(listener.handleEvent, listener);
383+
this.callback = async (...args) => {
384+
if (listener.handleEvent)
385+
await ReflectApply(listener.handleEvent, listener, args);
386+
};
385387
this.listener = listener;
386388
}
387389
}
@@ -463,7 +465,7 @@ class EventTarget {
463465
if (arguments.length < 2)
464466
throw new ERR_MISSING_ARGS('type', 'listener');
465467

466-
// We validateOptions before the shouldAddListeners check because the spec
468+
// We validateOptions before the validateListener check because the spec
467469
// requires us to hit getters.
468470
const {
469471
once,
@@ -474,7 +476,7 @@ class EventTarget {
474476
weak,
475477
} = validateEventListenerOptions(options);
476478

477-
if (!shouldAddListener(listener)) {
479+
if (!validateEventListener(listener)) {
478480
// The DOM silently allows passing undefined as a second argument
479481
// No error code for this since it is a Warning
480482
// eslint-disable-next-line no-restricted-syntax
@@ -547,7 +549,7 @@ class EventTarget {
547549
removeEventListener(type, listener, options = kEmptyObject) {
548550
if (!isEventTarget(this))
549551
throw new ERR_INVALID_THIS('EventTarget');
550-
if (!shouldAddListener(listener))
552+
if (!validateEventListener(listener))
551553
return;
552554

553555
type = String(type);
@@ -856,7 +858,7 @@ ObjectDefineProperties(NodeEventTarget.prototype, {
856858

857859
// EventTarget API
858860

859-
function shouldAddListener(listener) {
861+
function validateEventListener(listener) {
860862
if (typeof listener === 'function' ||
861863
typeof listener?.handleEvent === 'function') {
862864
return true;
@@ -865,6 +867,11 @@ function shouldAddListener(listener) {
865867
if (listener == null)
866868
return false;
867869

870+
if (typeof listener === 'object') {
871+
// Require `handleEvent` lazily.
872+
return true;
873+
}
874+
868875
throw new ERR_INVALID_ARG_TYPE('listener', 'EventListener', listener);
869876
}
870877

test/parallel/test-eventtarget.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,30 @@ let asyncTest = Promise.resolve();
232232
eventTarget.dispatchEvent(event);
233233
}
234234

235+
{
236+
const target = new EventTarget();
237+
const listener = {};
238+
// AddEventListener should not require handleEvent to be
239+
// defined on an EventListener.
240+
target.addEventListener('foo', listener);
241+
listener.handleEvent = common.mustCall(function(event) {
242+
strictEqual(event.type, 'foo');
243+
strictEqual(this, listener);
244+
});
245+
target.dispatchEvent(new Event('foo'));
246+
}
247+
248+
{
249+
const target = new EventTarget();
250+
const listener = {};
251+
// do not throw
252+
target.removeEventListener('foo', listener);
253+
target.addEventListener('foo', listener);
254+
target.removeEventListener('foo', listener);
255+
listener.handleEvent = common.mustNotCall();
256+
target.dispatchEvent(new Event('foo'));
257+
}
258+
235259
{
236260
const uncaughtException = common.mustCall((err, origin) => {
237261
strictEqual(err.message, 'boom');
@@ -308,7 +332,6 @@ let asyncTest = Promise.resolve();
308332
[
309333
'foo',
310334
1,
311-
{}, // No handleEvent function
312335
false,
313336
].forEach((i) => throws(() => target.addEventListener('foo', i), err(i)));
314337
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
'use strict';
2+
3+
require('../common');
4+
const { test, assert_equals, assert_unreached } =
5+
require('../common/wpt').harness;
6+
7+
// Manually ported from: https://github.com/web-platform-tests/wpt/blob/6cef1d2087d6a07d7cc6cee8cf207eec92e27c5f/dom/events/EventTarget-this-of-listener.html
8+
9+
// Mock document
10+
const document = {
11+
createElement: () => new EventTarget(),
12+
createTextNode: () => new EventTarget(),
13+
createDocumentFragment: () => new EventTarget(),
14+
createComment: () => new EventTarget(),
15+
createProcessingInstruction: () => new EventTarget(),
16+
};
17+
18+
test(() => {
19+
const nodes = [
20+
document.createElement('p'),
21+
document.createTextNode('some text'),
22+
document.createDocumentFragment(),
23+
document.createComment('a comment'),
24+
document.createProcessingInstruction('target', 'data'),
25+
];
26+
27+
let callCount = 0;
28+
for (const node of nodes) {
29+
node.addEventListener('someevent', function() {
30+
++callCount;
31+
assert_equals(this, node);
32+
});
33+
34+
node.dispatchEvent(new Event('someevent'));
35+
}
36+
37+
assert_equals(callCount, nodes.length);
38+
}, 'the this value inside the event listener callback should be the node');
39+
40+
test(() => {
41+
const nodes = [
42+
document.createElement('p'),
43+
document.createTextNode('some text'),
44+
document.createDocumentFragment(),
45+
document.createComment('a comment'),
46+
document.createProcessingInstruction('target', 'data'),
47+
];
48+
49+
let callCount = 0;
50+
for (const node of nodes) {
51+
const handler = {};
52+
53+
node.addEventListener('someevent', handler);
54+
handler.handleEvent = function() {
55+
++callCount;
56+
assert_equals(this, handler);
57+
};
58+
59+
node.dispatchEvent(new Event('someevent'));
60+
}
61+
62+
assert_equals(callCount, nodes.length);
63+
}, 'addEventListener should not require handleEvent to be defined on object listeners');
64+
65+
test(() => {
66+
const nodes = [
67+
document.createElement('p'),
68+
document.createTextNode('some text'),
69+
document.createDocumentFragment(),
70+
document.createComment('a comment'),
71+
document.createProcessingInstruction('target', 'data'),
72+
];
73+
74+
let callCount = 0;
75+
for (const node of nodes) {
76+
function handler() {
77+
++callCount;
78+
assert_equals(this, node);
79+
}
80+
81+
handler.handleEvent = () => {
82+
assert_unreached('should not call the handleEvent method on a function');
83+
};
84+
85+
node.addEventListener('someevent', handler);
86+
87+
node.dispatchEvent(new Event('someevent'));
88+
}
89+
90+
assert_equals(callCount, nodes.length);
91+
}, 'handleEvent properties added to a function before addEventListener are not reached');
92+
93+
test(() => {
94+
const nodes = [
95+
document.createElement('p'),
96+
document.createTextNode('some text'),
97+
document.createDocumentFragment(),
98+
document.createComment('a comment'),
99+
document.createProcessingInstruction('target', 'data'),
100+
];
101+
102+
let callCount = 0;
103+
for (const node of nodes) {
104+
function handler() {
105+
++callCount;
106+
assert_equals(this, node);
107+
}
108+
109+
node.addEventListener('someevent', handler);
110+
111+
handler.handleEvent = () => {
112+
assert_unreached('should not call the handleEvent method on a function');
113+
};
114+
115+
node.dispatchEvent(new Event('someevent'));
116+
}
117+
118+
assert_equals(callCount, nodes.length);
119+
}, 'handleEvent properties added to a function after addEventListener are not reached');

0 commit comments

Comments
 (0)