Skip to content

Commit 469dd54

Browse files
devversionjelbourn
authored andcommitted
fix(cdk/testing/testbed): synthetic DOM events cannot be prevented multiple times
Currently our harness infrastructure for TestBed relies on synthetic DOM events to simulate actions as `click()`. These events currently have special compatibility logic for `preventDefault()` that fails when invoked multiple times. It's totally valid that multiple event listeners call `event.preventDefault()` on the same event object. Fixes #19440.
1 parent 33f160c commit 469dd54

File tree

3 files changed

+67
-3
lines changed

3 files changed

+67
-3
lines changed

src/cdk/testing/testbed/BUILD.bazel

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//tools:defaults.bzl", "ts_library")
1+
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")
22

33
package(default_visibility = ["//visibility:public"])
44

@@ -21,3 +21,16 @@ filegroup(
2121
name = "source-files",
2222
srcs = glob(["**/*.ts"]),
2323
)
24+
25+
ng_test_library(
26+
name = "unit_test_sources",
27+
srcs = glob(["**/*.spec.ts"]),
28+
deps = [
29+
":testbed",
30+
],
31+
)
32+
33+
ng_web_test_suite(
34+
name = "unit_tests",
35+
deps = [":unit_test_sources"],
36+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {createKeyboardEvent, createMouseEvent} from './event-objects';
2+
3+
describe('event objects', () => {
4+
let testElement: HTMLElement;
5+
6+
beforeEach(() => testElement = document.createElement('div'));
7+
8+
describe('synthetic mouse event', () => {
9+
10+
it('should be possible to call `preventDefault` multiple times', () => {
11+
const preventDefaultSpy = jasmine.createSpy('preventDefault').and
12+
.callFake((event: Event) => event.preventDefault());
13+
14+
// Register event listeners twice, where both prevent prevent the default behavior.
15+
testElement.addEventListener('click', (event: Event) => preventDefaultSpy(event));
16+
testElement.addEventListener('click', (event: Event) => preventDefaultSpy(event));
17+
18+
expect(() => {
19+
// Dispatch a synthetic mouse click event on the test element.
20+
testElement.dispatchEvent(createMouseEvent('click'));
21+
}).not.toThrow();
22+
expect(preventDefaultSpy).toHaveBeenCalledTimes(2);
23+
});
24+
});
25+
26+
describe('synthetic keyboard event', () => {
27+
28+
it('should be possible to call `preventDefault` multiple times', () => {
29+
const preventDefaultSpy = jasmine.createSpy('preventDefault').and
30+
.callFake((event: Event) => event.preventDefault());
31+
32+
// Register event listeners twice, where both prevent prevent the default behavior.
33+
testElement.addEventListener('keydown', (event: Event) => preventDefaultSpy(event));
34+
testElement.addEventListener('keydown', (event: Event) => preventDefaultSpy(event));
35+
36+
expect(() => {
37+
// Dispatch a synthetic keyboard down event on the test element.
38+
testElement.dispatchEvent(createKeyboardEvent('keydown'));
39+
}).not.toThrow();
40+
expect(preventDefaultSpy).toHaveBeenCalledTimes(2);
41+
});
42+
});
43+
});

src/cdk/testing/testbed/fake-events/event-objects.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function createMouseEvent(type: string, clientX = 0, clientY = 0, button
4545

4646
// IE won't set `defaultPrevented` on synthetic events so we need to do it manually.
4747
event.preventDefault = function() {
48-
Object.defineProperty(event, 'defaultPrevented', { get: () => true });
48+
defineReadonlyEventProperty(event, 'defaultPrevented', true);
4949
return originalPreventDefault();
5050
};
5151

@@ -157,7 +157,7 @@ export function createKeyboardEvent(type: string, keyCode: number = 0, key: stri
157157

158158
// IE won't set `defaultPrevented` on synthetic events so we need to do it manually.
159159
event.preventDefault = function() {
160-
Object.defineProperty(event, 'defaultPrevented', { get: () => true });
160+
defineReadonlyEventProperty(event, 'defaultPrevented', true);
161161
return originalPreventDefault.apply(this, arguments);
162162
};
163163

@@ -173,3 +173,11 @@ export function createFakeEvent(type: string, canBubble = false, cancelable = tr
173173
event.initEvent(type, canBubble, cancelable);
174174
return event;
175175
}
176+
177+
/**
178+
* Defines a readonly property on the given event object. Readonly properties on an event object
179+
* are always set as configurable as that matches default readonly properties for DOM event objects.
180+
*/
181+
function defineReadonlyEventProperty(event: Event, propertyName: string, value: any) {
182+
Object.defineProperty(event, propertyName, {get: () => value, configurable: true});
183+
}

0 commit comments

Comments
 (0)