Skip to content
File

Blob: src/workerd/api/tests/events-test.js

javascript450 lines
1// Copyright (c) 2023 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import { deepStrictEqual, strictEqual, throws, ok } from 'node:assert';
5import { mock } from 'node:test';
6 
7// Test for the Event and EventTarget standard Web API implementations.
8// The implementation for these are in api/basics.{h|c++}
9 
10export const event = {
11 test() {
12 // Any value that is not-stringifiable fails
13 throws(() => new Event(Symbol('test')));
14 
15 // stringifiable values do work, however
16 strictEqual(new Event({}).type, '[object Object]');
17 strictEqual(new Event(null).type, 'null');
18 strictEqual(new Event(1).type, '1');
19 
20 // Passing undefined explicitly works
21 strictEqual(new Event(undefined).type, 'undefined');
22 
23 // But not passing a value for type fails
24 throws(() => new Event());
25 
26 // We can create an Event object with no options and it works as expected.
27 const event = new Event('foo');
28 strictEqual(event.type, 'foo');
29 strictEqual(event.bubbles, false);
30 strictEqual(event.cancelable, false);
31 strictEqual(event.composed, false);
32 strictEqual(event.isTrusted, false);
33 strictEqual(event.defaultPrevented, false);
34 strictEqual(event.eventPhase, Event.NONE);
35 strictEqual(event.returnValue, true);
36 strictEqual(event.timeStamp, 0.0);
37 strictEqual(event.cancelBubble, false);
38 strictEqual(event.currentTarget, undefined);
39 deepStrictEqual(event.composedPath(), []);
40 event.stopImmediatePropagation();
41 event.stopPropagation();
42 event.preventDefault();
43 // Even tho we called preventDefault, because the event is not cancelable
44 // by default, defaultPrevented still is false.
45 strictEqual(event.defaultPrevented, false);
46 strictEqual(event.cancelBubble, true);
47 },
48};
49 
50export const eventWithOptions = {
51 test() {
52 // The options argument must be an object
53 throws(() => new Event('foo', 1));
54 throws(() => new Event('foo', 'bar'));
55 
56 // We can create an Event object with no options and it works as expected.
57 const event = new Event('foo', {
58 cancelable: true,
59 bubbles: 'truthy values work also',
60 composed: true,
61 });
62 strictEqual(event.type, 'foo');
63 strictEqual(event.bubbles, true);
64 strictEqual(event.cancelable, true);
65 strictEqual(event.composed, true);
66 strictEqual(event.isTrusted, false);
67 strictEqual(event.defaultPrevented, false);
68 strictEqual(event.eventPhase, Event.NONE);
69 strictEqual(event.returnValue, true);
70 strictEqual(event.timeStamp, 0.0);
71 strictEqual(event.cancelBubble, false);
72 strictEqual(event.currentTarget, undefined);
73 deepStrictEqual(event.composedPath(), []);
74 event.stopImmediatePropagation();
75 event.stopPropagation();
76 event.preventDefault();
77 // Because the event is cancelable, defaultPrevented is true.
78 strictEqual(event.defaultPrevented, true);
79 strictEqual(event.returnValue, false);
80 },
81};
82 
83export const eventSubclass = {
84 test() {
85 class Foo extends Event {
86 constructor() {
87 super('foo');
88 }
89 }
90 const event = new Foo();
91 strictEqual(event.type, 'foo');
92 strictEqual(event.bubbles, false);
93 strictEqual(event.cancelable, false);
94 strictEqual(event.composed, false);
95 strictEqual(event.isTrusted, false);
96 strictEqual(event.defaultPrevented, false);
97 strictEqual(event.eventPhase, Event.NONE);
98 strictEqual(event.returnValue, true);
99 strictEqual(event.timeStamp, 0.0);
100 strictEqual(event.cancelBubble, false);
101 strictEqual(event.currentTarget, undefined);
102 
103 // Everything except cancelBubble is read only and will throw
104 // if attempts are made to modify
105 throws(() => (event.type = 'foo'));
106 throws(() => (event.bubbles = false));
107 throws(() => (event.cancelable = false));
108 throws(() => (event.composed = false));
109 throws(() => (event.isTrusted = false));
110 throws(() => (event.defaultPrevented = false));
111 throws(() => (event.eventPhase = Event.NONE));
112 throws(() => (event.returnValue = true));
113 throws(() => (event.timeStamp = 0.0));
114 throws(() => (event.currentTarget = undefined));
115 event.cancelBubble = true;
116 strictEqual(event.cancelBubble, true);
117 
118 // With the default compatibility flag set, the properties should
119 // exist on the prototype and not as own properties on the event itself.
120 strictEqual(
121 Reflect.getOwnPropertyDescriptor(event, 'cancelable'),
122 undefined
123 );
124 
125 // Which means a subclass can replace the implementation successfully.
126 class Bar extends Event {
127 constructor() {
128 super('bar');
129 }
130 get bubbles() {
131 return 'hello';
132 }
133 }
134 const bar = new Bar();
135 strictEqual(bar.bubbles, 'hello');
136 strictEqual(bar.composed, false);
137 },
138};
139 
140export const extendableEventNotConstructable = {
141 test() {
142 // While the spec defines ExtendableEvent to be consructable, we do not support
143 // doing so. This is intentional because the only real use case of ExtendableEvent
144 // is to allow calling waitUntil, which only works on trusted events which can only
145 // originate from the runtime. That is, user code cannot create their own trusted
146 // events.
147 strictEqual(typeof ExtendableEvent, 'function');
148 throws(() => new ExtendableEvent('foo'));
149 },
150};
151 
152export const basicEventTarget = {
153 test() {
154 const target = new EventTarget();
155 
156 const event = new Event('foo');
157 strictEqual(event.eventPhase, Event.NONE);
158 strictEqual(event.currentTarget, undefined);
159 
160 let dispatchCount = 0;
161 
162 const handler = (dispatched) => {
163 strictEqual(dispatched, event);
164 strictEqual(dispatched.eventPhase, Event.AT_TARGET);
165 strictEqual(dispatched.currentTarget, target);
166 deepStrictEqual(dispatched.composedPath(), [target]);
167 dispatchCount++;
168 
169 // The event is already being dispatched so can't be again.
170 throws(() => target.dispatchEvent(dispatched));
171 };
172 
173 const handlerObj = {
174 handleEvent: handler,
175 };
176 
177 throws(() => target.addEventListener('foo', {}));
178 throws(() => target.addEventListener('foo', 'hello'));
179 throws(() => target.addEventListener('foo', []));
180 throws(() => target.addEventListener('foo', false));
181 
182 // Event listener with no options
183 target.addEventListener('foo', handler);
184 
185 // Same handler can be attached twice, but is only invoked once.
186 target.addEventListener('foo', handler);
187 
188 target.addEventListener('foo', handlerObj);
189 
190 let classCalled;
191 const foo = new (class Foo {
192 handleEvent(event) {
193 classCalled = true;
194 }
195 })();
196 target.addEventListener('foo', foo);
197 
198 target.dispatchEvent(event);
199 
200 strictEqual(classCalled, true);
201 strictEqual(event.eventPhase, Event.NONE);
202 strictEqual(event.currentTarget, target);
203 
204 strictEqual(dispatchCount, 2);
205 
206 target.removeEventListener('foo', handler);
207 
208 target.dispatchEvent(event);
209 
210 strictEqual(dispatchCount, 3);
211 },
212};
213 
214export const subclassedEventTarget = {
215 test() {
216 class MyEventTarget extends EventTarget {}
217 const event = new Event('foo');
218 const target = new MyEventTarget();
219 let dispatchCount = 0;
220 target.addEventListener('foo', (dispatched) => {
221 strictEqual(dispatched, event);
222 dispatchCount++;
223 });
224 target.dispatchEvent(event);
225 strictEqual(dispatchCount, 1);
226 },
227};
228 
229export const onceListener = {
230 test() {
231 const target = new EventTarget();
232 const event = new Event('foo');
233 
234 let dispatchCount = 0;
235 
236 target.addEventListener(
237 'foo',
238 () => {
239 dispatchCount++;
240 },
241 { once: true }
242 );
243 
244 target.dispatchEvent(event);
245 target.dispatchEvent(event);
246 
247 strictEqual(dispatchCount, 1);
248 },
249};
250 
251export const cancelableListener = {
252 test() {
253 const target = new EventTarget();
254 const event = new Event('foo');
255 
256 let dispatchCount = 0;
257 
258 const ac = new AbortController();
259 
260 target.addEventListener(
261 'foo',
262 () => {
263 dispatchCount++;
264 },
265 { signal: ac.signal }
266 );
267 
268 // Passing an already aborted signal just works as expected.
269 // No errors are thrown.
270 target.addEventListener(
271 'foo',
272 () => {
273 dispatchCount++;
274 },
275 { signal: AbortSignal.abort() }
276 );
277 
278 ac.abort();
279 
280 target.dispatchEvent(event);
281 
282 strictEqual(dispatchCount, 0);
283 },
284};
285 
286export const cancelableListenerAbortPropagation = {
287 test() {
288 // TODO(bug): Cancelable event listeners should be removed by signal even when
289 // signal's abort event propagation is stopped. This is a safety measure to
290 // prevent certain kinds of memory leaks. We currently do not implement this
291 // protection.
292 // const controller = new AbortController();
293 // const { signal } = controller;
294 // signal.addEventListener('abort', (e) => e.stopImmediatePropagation(), { once: true });
295 // const et = new EventTarget();
296 // et.addEventListener('foo', () => {
297 // console.log('....')
298 // throw new Error('should not be called');
299 // }, { signal });
300 // controller.abort();
301 // et.dispatchEvent(new Event('foo'));
302 },
303};
304 
305export const passiveCaptureListener = {
306 test() {
307 const target = new EventTarget();
308 // capture and passive must be false. We do not support these but
309 // we allow them to be set for code portability reasons.
310 throws(() => {
311 target.addEventListener('foo', () => {}, {
312 capture: true,
313 });
314 });
315 throws(() => {
316 target.addEventListener('foo', () => {}, true);
317 });
318 throws(() => {
319 target.addEventListener('foo', () => {}, {
320 passive: true,
321 });
322 });
323 throws(() => {
324 target.removeEventListener('foo', () => {}, {
325 capture: true,
326 });
327 });
328 throws(() => {
329 target.removeEventListener('foo', () => {}, true);
330 });
331 },
332};
333 
334export const globalIsEventTarget = {
335 test() {
336 // TODO(bug): For some reason, even tho our globalThis does, in fact,
337 // extend EventTarget and inherits the dispatchEvent, addEventListener, etc,
338 // instanceof does not report that fact correctly. So we'll need to fix this.
339 
340 // strictEqual(globalThis instanceof EventTarget);
341 
342 strictEqual(typeof globalThis.dispatchEvent, 'function');
343 strictEqual(typeof globalThis.addEventListener, 'function');
344 strictEqual(typeof globalThis.removeEventListener, 'function');
345 
346 const event = new Event('foo');
347 let dispatchCount = 0;
348 addEventListener(
349 'foo',
350 () => {
351 dispatchCount++;
352 },
353 { once: true }
354 );
355 dispatchEvent(event);
356 strictEqual(dispatchCount, 1);
357 },
358};
359 
360export const errorInHandler = {
361 test() {
362 // TODO(bug): Erroring in one event handler should not prevent others from being
363 // run but we currently do not implement this correctly.
364 const event = new Event('foo');
365 const target = new EventTarget();
366 let dispatchCount = 0;
367 target.addEventListener('foo', () => {
368 dispatchCount++;
369 throw new Error('boom');
370 });
371 target.addEventListener('foo', () => {
372 dispatchCount++;
373 });
374 
375 throws(() => target.dispatchEvent(event));
376 
377 // The dispatchCount here should be 2, but with the current bug, it's only 1
378 // strictEqual(dispatchCount, 2);
379 strictEqual(dispatchCount, 1);
380 },
381};
382 
383export const stopImmediatePropagation = {
384 test() {
385 const event = new Event('foo');
386 const target = new EventTarget();
387 let dispatchCount = 0;
388 target.addEventListener('foo', (event) => {
389 dispatchCount++;
390 // Calling stopImmediatePropagation should prevent the next listener
391 // from being invoked.
392 event.stopImmediatePropagation();
393 });
394 target.addEventListener('foo', (event) => {
395 throw new Error('should not have been invoked');
396 });
397 target.dispatchEvent(event);
398 strictEqual(dispatchCount, 1);
399 },
400};
401 
402export const nullUndefinedHandler = {
403 test() {
404 // TODO(bug): Odd as it may seem, the spec allows passing null and undefined
405 // to addEventListener. We currently do not handle these correctly.
406 const _target = new EventTarget();
407 // target.addEventListener('foo', null);
408 // target.addEventListener('foo', undefined);
409 },
410};
411 
412export const customEvent = {
413 test() {
414 const event = new CustomEvent('foo', { detail: { a: 123 } });
415 ok(event instanceof Event);
416 strictEqual(event.type, 'foo');
417 deepStrictEqual(event.detail, { a: 123 });
418 },
419};
420 
421export const closeEvent = {
422 test() {
423 // The CloseEvent constructor second argument is optional. Our implementation
424 // had it as required. Let's make sure we can create it without the second arg.
425 new CloseEvent('foo');
426 new CloseEvent('foo', { code: 1000, reason: 'bye' });
427 },
428};
429 
430export const handlerThis = {
431 test() {
432 const et = new EventTarget();
433 const handler = mock.fn(function () {
434 strictEqual(this, et);
435 });
436 et.addEventListener('foo', handler);
437 
438 const handlerObject = {
439 handleEvent: mock.fn(function () {
440 strictEqual(this, handlerObject);
441 }),
442 };
443 et.addEventListener('foo', handlerObject);
444 
445 et.dispatchEvent(new Event('foo'));
446 strictEqual(handler.mock.callCount(), 1);
447 strictEqual(handlerObject.handleEvent.mock.callCount(), 1);
448 },
449};