Skip to content

Commit 7a7e6fe

Browse files
committed
feat(cdk/testing): add the ability to dispatch arbitrary events
Allows for arbitrary events to be dispatched through a `TestElement`. This covers the following use cases: 1. Some events can't be simulated easily without a user interaction (e.g. `change` event on an `input`). 2. Allows for custom DOM event handlers to be triggered.
1 parent b3f1fb3 commit 7a7e6fe

File tree

10 files changed

+71
-2
lines changed

10 files changed

+71
-2
lines changed

src/cdk/testing/protractor/protractor-element.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,24 @@ export class ProtractorElement implements TestElement {
198198
async isFocused(): Promise<boolean> {
199199
return this.element.equals(browser.driver.switchTo().activeElement());
200200
}
201+
202+
async dispatchEvent(name: string, data?: Record<string | number, any>): Promise<void> {
203+
return browser.executeScript(_dispatchEvent, name, data ? JSON.stringify(data) : null,
204+
this.element);
205+
}
206+
}
207+
208+
/**
209+
* Dispatches an event with a particular name and data to an element.
210+
* Note that this needs to be a pure function, because it gets stringified by
211+
* Protractor and is executed inside the browser.
212+
*/
213+
function _dispatchEvent(name: string, data: string | null, element: ElementFinder) {
214+
const event = document.createEvent('Event');
215+
event.initEvent(name);
216+
if (data) {
217+
// tslint:disable-next-line:ban Have to use `Object.assign` to preserve the original object.
218+
Object.assign(event, JSON.parse(data));
219+
}
220+
element.dispatchEvent(event);
201221
}

src/cdk/testing/test-element.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ export interface TestElement {
147147
* @breaking-change 12.0.0 To become a required method.
148148
*/
149149
selectOptions?(...optionIndexes: number[]): Promise<void>;
150+
151+
/**
152+
* Dispatches an event with a particular name. Optionally attaches data to the event object.
153+
* @param name Name of the event to be dispatched.
154+
* @param data Data to be attached to the event object.
155+
* @breaking-change 12.0.0 To be a required method.
156+
*/
157+
dispatchEvent?(name: string, data?: Record<string | number, any>): Promise<void>;
150158
}
151159

152160
export interface TextOptions {

src/cdk/testing/testbed/unit-test-element.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
triggerBlur,
2525
triggerFocus,
2626
typeInElement,
27+
dispatchEvent,
28+
createFakeEvent
2729
} from './fake-events';
2830

2931
/** Maps `TestKey` constants to the `keyCode` and `key` values used by native browser events. */
@@ -200,6 +202,18 @@ export class UnitTestElement implements TestElement {
200202
return document.activeElement === this.element;
201203
}
202204

205+
async dispatchEvent(name: string, data?: Record<string | number, any>): Promise<void> {
206+
const event = createFakeEvent(name);
207+
208+
if (data) {
209+
// tslint:disable-next-line:ban Have to use `Object.assign` to preserve the original object.
210+
Object.assign(event, data);
211+
}
212+
213+
dispatchEvent(this.element, event);
214+
await this._stabilize();
215+
}
216+
203217
/**
204218
* Dispatches a pointer event on the current element if the browser supports it.
205219
* @param name Name of the pointer event to be dispatched.

src/cdk/testing/tests/cross-environment.spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,6 @@ export function crossEnvironmentSpecs(
445445
expect(await button.matchesSelector('button:disabled')).toBe(false);
446446
});
447447

448-
449448
it('should get correct text excluding certain selectors', async () => {
450449
const results = await harness.subcomponentAndSpecialHarnesses();
451450
const subHarnessHost = await results[0].host();
@@ -454,6 +453,22 @@ export function crossEnvironmentSpecs(
454453
expect(await subHarnessHost.text({exclude: 'li'})).toBe('List of test tools');
455454
});
456455

456+
it('should dispatch a basic custom event', async () => {
457+
const target = await harness.customEventBasic();
458+
459+
// @breaking-change 12.0.0 Remove non-null assertion once `dispatchEvent` is required.
460+
await target.dispatchEvent!('myCustomEvent');
461+
expect(await target.text()).toBe('Basic event: 1');
462+
});
463+
464+
it('should dispatch a custom event with attached data', async () => {
465+
const target = await harness.customEventObject();
466+
467+
// @breaking-change 12.0.0 Remove non-null assertion once `dispatchEvent` is required.
468+
await target.dispatchEvent!('myCustomEvent', {message: 'Hello', value: 1337});
469+
expect(await target.text()).toBe('Event with object: {"message":"Hello","value":1337}');
470+
});
471+
457472
it('should get TestElements and ComponentHarnesses', async () => {
458473
const results = await harness.subcomponentHarnessesAndElements();
459474
expect(results.length).toBe(5);

src/cdk/testing/tests/harnesses/main-component-harness.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export class MainComponentHarness extends ComponentHarness {
9191
readonly deepShadow = this.locatorFor(
9292
'test-shadow-boundary test-sub-shadow-boundary > .in-the-shadows');
9393
readonly hoverTest = this.locatorFor('#hover-box');
94+
readonly customEventBasic = this.locatorFor('#custom-event-basic');
95+
readonly customEventObject = this.locatorFor('#custom-event-object');
9496

9597
private _testTools = this.locatorFor(SubComponentHarness);
9698

src/cdk/testing/tests/test-main-component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ <h1 style="height: 100px; width: 200px;">Main Component</h1>
4242
<div id="multi-select-value">Multi-select: {{multiSelect}}</div>
4343
<div id="multi-select-change-counter">Change events: {{multiSelectChangeEventCount}}</div>
4444

45+
<div (myCustomEvent)="basicEvent = basicEvent + 1" id="custom-event-basic">Basic event: {{basicEvent}}</div>
46+
<div (myCustomEvent)="onCustomEvent($event)" id="custom-event-object">Event with object: {{customEventData}}</div>
4547
</div>
4648
<div class="subcomponents">
4749
<test-sub class="test-special" title="test tools" [items]="testTools"></test-sub>

src/cdk/testing/tests/test-main-component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
encapsulation: ViewEncapsulation.None,
2626
changeDetection: ChangeDetectionStrategy.OnPush,
2727
})
28-
2928
export class TestMainComponent implements OnDestroy {
3029
username: string;
3130
counter: number;
@@ -42,6 +41,8 @@ export class TestMainComponent implements OnDestroy {
4241
singleSelectChangeEventCount = 0;
4342
multiSelect: string[] = [];
4443
multiSelectChangeEventCount = 0;
44+
basicEvent = 0;
45+
customEventData: string | null = null;
4546
_shadowDomSupported = _supportsShadowDom();
4647

4748
@ViewChild('clickTestElement') clickTestElement: ElementRef<HTMLElement>;
@@ -94,6 +95,10 @@ export class TestMainComponent implements OnDestroy {
9495
this.relativeY = Math.round(event.clientY - top);
9596
}
9697

98+
onCustomEvent(event: any) {
99+
this.customEventData = JSON.stringify({message: event.message, value: event.value});
100+
}
101+
97102
runTaskOutsideZone() {
98103
this._zone.runOutsideAngular(() => setTimeout(() => {
99104
this.taskStateResultElement.nativeElement.textContent = 'result';

tools/public_api_guard/cdk/testing.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export interface TestElement {
135135
click(): Promise<void>;
136136
click(location: 'center'): Promise<void>;
137137
click(relativeX: number, relativeY: number): Promise<void>;
138+
dispatchEvent?(name: string, data?: Record<string | number, any>): Promise<void>;
138139
focus(): Promise<void>;
139140
getAttribute(name: string): Promise<string | null>;
140141
getCssValue(property: string): Promise<string>;

tools/public_api_guard/cdk/testing/protractor.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export declare class ProtractorElement implements TestElement {
44
blur(): Promise<void>;
55
clear(): Promise<void>;
66
click(...args: [] | ['center'] | [number, number]): Promise<void>;
7+
dispatchEvent(name: string, data?: Record<string | number, any>): Promise<void>;
78
focus(): Promise<void>;
89
getAttribute(name: string): Promise<string | null>;
910
getCssValue(property: string): Promise<string>;

tools/public_api_guard/cdk/testing/testbed.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export declare class UnitTestElement implements TestElement {
2222
blur(): Promise<void>;
2323
clear(): Promise<void>;
2424
click(...args: [] | ['center'] | [number, number]): Promise<void>;
25+
dispatchEvent(name: string, data?: Record<string | number, any>): Promise<void>;
2526
focus(): Promise<void>;
2627
getAttribute(name: string): Promise<string | null>;
2728
getCssValue(property: string): Promise<string>;

0 commit comments

Comments
 (0)