Skip to content

Commit f99d0dd

Browse files
authored
feat(cdk/testing): allow for data to be attached to custom event (#20764)
Allows for data to be attached to an event that is triggered through `TestElement.dispatchEvent`. This is required in order to simulate some events like `animationend`. It is limited to primitive data types, because the data has to be JSON-serializeable for Protractor.
1 parent 4263cac commit f99d0dd

File tree

10 files changed

+51
-11
lines changed

10 files changed

+51
-11
lines changed

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
ModifierKeys,
1313
TestElement,
1414
TestKey,
15-
TextOptions
15+
TextOptions,
16+
EventData,
1617
} from '@angular/cdk/testing';
1718
import {browser, Button, by, ElementFinder, Key} from 'protractor';
1819

@@ -195,8 +196,8 @@ export class ProtractorElement implements TestElement {
195196
return this.element.equals(browser.driver.switchTo().activeElement());
196197
}
197198

198-
async dispatchEvent(name: string): Promise<void> {
199-
return browser.executeScript(_dispatchEvent, name, this.element);
199+
async dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void> {
200+
return browser.executeScript(_dispatchEvent, name, this.element, data);
200201
}
201202

202203
/** Dispatches all the events that are part of a click event sequence. */
@@ -220,9 +221,15 @@ export class ProtractorElement implements TestElement {
220221
* Note that this needs to be a pure function, because it gets stringified by
221222
* Protractor and is executed inside the browser.
222223
*/
223-
function _dispatchEvent(name: string, element: ElementFinder) {
224+
function _dispatchEvent(name: string, element: ElementFinder, data?: Record<string, EventData>) {
224225
const event = document.createEvent('Event');
225226
event.initEvent(name);
227+
228+
if (data) {
229+
// tslint:disable-next-line:ban Have to use `Object.assign` to preserve the original object.
230+
Object.assign(event, data);
231+
}
232+
226233
// This type has a string index signature, so we cannot access it using a dotted property access.
227234
element['dispatchEvent'](event);
228235
}

src/cdk/testing/test-element.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export interface ModifierKeys {
1616
meta?: boolean;
1717
}
1818

19+
/** Data that can be attached to a custom event dispatched from a `TestElement`. */
20+
export type EventData =
21+
string | number | boolean | undefined | null | EventData[] | {[key: string]: EventData};
22+
1923
/** An enum of non-text keys that can be used with the `sendKeys` method. */
2024
// NOTE: This is a separate enum from `@angular/cdk/keycodes` because we don't necessarily want to
2125
// support every possible keyCode. We also can't rely on Protractor's `Key` because we don't want a
@@ -161,7 +165,7 @@ export interface TestElement {
161165
* @param name Name of the event to be dispatched.
162166
* @breaking-change 12.0.0 To be a required method.
163167
*/
164-
dispatchEvent?(name: string): Promise<void>;
168+
dispatchEvent?(name: string, data?: Record<string, EventData>): Promise<void>;
165169
}
166170

167171
export interface TextOptions {

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@ import {
1313
ModifierKeys,
1414
TestElement,
1515
TestKey,
16-
TextOptions
16+
TextOptions,
17+
EventData,
1718
} from '@angular/cdk/testing';
1819
import {
1920
clearElement,
21+
createFakeEvent,
2022
dispatchFakeEvent,
2123
dispatchMouseEvent,
2224
dispatchPointerEvent,
2325
isTextInput,
2426
triggerBlur,
2527
triggerFocus,
2628
typeInElement,
29+
dispatchEvent,
2730
} from './fake-events';
2831

2932
/** Maps `TestKey` constants to the `keyCode` and `key` values used by native browser events. */
@@ -188,8 +191,15 @@ export class UnitTestElement implements TestElement {
188191
return document.activeElement === this.element;
189192
}
190193

191-
async dispatchEvent(name: string): Promise<void> {
192-
dispatchFakeEvent(this.element, name);
194+
async dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void> {
195+
const event = createFakeEvent(name);
196+
197+
if (data) {
198+
// tslint:disable-next-line:ban Have to use `Object.assign` to preserve the original object.
199+
Object.assign(event, data);
200+
}
201+
202+
dispatchEvent(this.element, event);
193203
await this._stabilize();
194204
}
195205

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,14 @@ export function crossEnvironmentSpecs(
468468
expect(await target.text()).toBe('Basic event: 1');
469469
});
470470

471+
it('should dispatch a custom event with attached data', async () => {
472+
const target = await harness.customEventObject();
473+
474+
// @breaking-change 12.0.0 Remove non-null assertion once `dispatchEvent` is required.
475+
await target.dispatchEvent!('myCustomEvent', {message: 'Hello', value: 1337});
476+
expect(await target.text()).toBe('Event with object: {"message":"Hello","value":1337}');
477+
});
478+
471479
it('should get TestElements and ComponentHarnesses', async () => {
472480
const results = await harness.subcomponentHarnessesAndElements();
473481
expect(results.length).toBe(5);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export class MainComponentHarness extends ComponentHarness {
9393
'test-shadow-boundary test-sub-shadow-boundary > .in-the-shadows');
9494
readonly hoverTest = this.locatorFor('#hover-box');
9595
readonly customEventBasic = this.locatorFor('#custom-event-basic');
96+
readonly customEventObject = this.locatorFor('#custom-event-object');
9697

9798
private _testTools = this.locatorFor(SubComponentHarness);
9899

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ <h1 style="height: 100px; width: 200px;">Main Component</h1>
4343
<div id="multi-select-value">Multi-select: {{multiSelect}}</div>
4444
<div id="multi-select-change-counter">Change events: {{multiSelectChangeEventCount}}</div>
4545
<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>
4647
</div>
4748
<div class="subcomponents">
4849
<test-sub class="test-special" title="test tools" [items]="testTools"></test-sub>

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class TestMainComponent implements OnDestroy {
4040
multiSelect: string[] = [];
4141
multiSelectChangeEventCount = 0;
4242
basicEvent = 0;
43+
customEventData: string | null = null;
4344
_shadowDomSupported = _supportsShadowDom();
4445
clickResult = {x: -1, y: -1};
4546
rightClickResult = {x: -1, y: -1, button: -1};
@@ -97,6 +98,10 @@ export class TestMainComponent implements OnDestroy {
9798
this._assignRelativeCoordinates(event, this.rightClickResult);
9899
}
99100

101+
onCustomEvent(event: any) {
102+
this.customEventData = JSON.stringify({message: event.message, value: event.value});
103+
}
104+
100105
runTaskOutsideZone() {
101106
this._zone.runOutsideAngular(() => setTimeout(() => {
102107
this.taskStateResultElement.nativeElement.textContent = 'result';

tools/public_api_guard/cdk/testing.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export interface ElementDimensions {
4848
width: number;
4949
}
5050

51+
export declare type EventData = string | number | boolean | undefined | null | EventData[] | {
52+
[key: string]: EventData;
53+
};
54+
5155
export declare function handleAutoChangeDetectionStatus(handler: (status: AutoChangeDetectionStatus) => void): void;
5256

5357
export declare abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFactory {
@@ -135,7 +139,7 @@ export interface TestElement {
135139
click(): Promise<void>;
136140
click(location: 'center'): Promise<void>;
137141
click(relativeX: number, relativeY: number): Promise<void>;
138-
dispatchEvent?(name: string): Promise<void>;
142+
dispatchEvent?(name: string, data?: Record<string, EventData>): Promise<void>;
139143
focus(): Promise<void>;
140144
getAttribute(name: string): Promise<string | null>;
141145
getCssValue(property: string): Promise<string>;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +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): Promise<void>;
7+
dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void>;
88
focus(): Promise<void>;
99
getAttribute(name: string): Promise<string | null>;
1010
getCssValue(property: string): Promise<string>;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +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): Promise<void>;
25+
dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void>;
2626
focus(): Promise<void>;
2727
getAttribute(name: string): Promise<string | null>;
2828
getCssValue(property: string): Promise<string>;

0 commit comments

Comments
 (0)