File
Blob: src/workerd/api/tests/events-test.js
| 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 |
| 4 | import { deepStrictEqual, strictEqual, throws, ok } from 'node:assert'; |
| 5 | import { 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 | |
| 10 | export 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 | |
| 50 | export 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 | |
| 83 | export 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 | |
| 140 | export 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 | |
| 152 | export 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 | |
| 214 | export 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 | |
| 229 | export 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 | |
| 251 | export 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 | |
| 286 | export 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 | |
| 305 | export 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 | |
| 334 | export 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 | |
| 360 | export 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 | |
| 383 | export 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 | |
| 402 | export 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 | |
| 412 | export 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 | |
| 421 | export 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 | |
| 430 | export 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 | }; |