Skip to content

feat(cdk/testing): allow for data to be attached to custom event #20764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/cdk/testing/protractor/protractor-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
ModifierKeys,
TestElement,
TestKey,
TextOptions
TextOptions,
EventData,
} from '@angular/cdk/testing';
import {browser, by, ElementFinder, Key} from 'protractor';

Expand Down Expand Up @@ -199,8 +200,8 @@ export class ProtractorElement implements TestElement {
return this.element.equals(browser.driver.switchTo().activeElement());
}

async dispatchEvent(name: string): Promise<void> {
return browser.executeScript(_dispatchEvent, name, this.element);
async dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void> {
return browser.executeScript(_dispatchEvent, name, this.element, data);
}
}

Expand All @@ -209,9 +210,15 @@ export class ProtractorElement implements TestElement {
* Note that this needs to be a pure function, because it gets stringified by
* Protractor and is executed inside the browser.
*/
function _dispatchEvent(name: string, element: ElementFinder) {
function _dispatchEvent(name: string, element: ElementFinder, data?: Record<string, EventData>) {
const event = document.createEvent('Event');
event.initEvent(name);

if (data) {
// tslint:disable-next-line:ban Have to use `Object.assign` to preserve the original object.
Object.assign(event, data);
}

// This type has a string index signature, so we cannot access it using a dotted property access.
element['dispatchEvent'](event);
}
6 changes: 5 additions & 1 deletion src/cdk/testing/test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface ModifierKeys {
meta?: boolean;
}

/** Data that can be attached to a custom event dispatched from a `TestElement`. */
export type EventData =
string | number | boolean | undefined | null | EventData[] | {[key: string]: EventData};

/** An enum of non-text keys that can be used with the `sendKeys` method. */
// NOTE: This is a separate enum from `@angular/cdk/keycodes` because we don't necessarily want to
// support every possible keyCode. We also can't rely on Protractor's `Key` because we don't want a
Expand Down Expand Up @@ -153,7 +157,7 @@ export interface TestElement {
* @param name Name of the event to be dispatched.
* @breaking-change 12.0.0 To be a required method.
*/
dispatchEvent?(name: string): Promise<void>;
dispatchEvent?(name: string, data?: Record<string, EventData>): Promise<void>;
}

export interface TextOptions {
Expand Down
16 changes: 13 additions & 3 deletions src/cdk/testing/testbed/unit-test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ import {
ModifierKeys,
TestElement,
TestKey,
TextOptions
TextOptions,
EventData,
} from '@angular/cdk/testing';
import {
clearElement,
createFakeEvent,
dispatchFakeEvent,
dispatchMouseEvent,
dispatchPointerEvent,
isTextInput,
triggerBlur,
triggerFocus,
typeInElement,
dispatchEvent,
} from './fake-events';

/** Maps `TestKey` constants to the `keyCode` and `key` values used by native browser events. */
Expand Down Expand Up @@ -200,8 +203,15 @@ export class UnitTestElement implements TestElement {
return document.activeElement === this.element;
}

async dispatchEvent(name: string): Promise<void> {
dispatchFakeEvent(this.element, name);
async dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void> {
const event = createFakeEvent(name);

if (data) {
// tslint:disable-next-line:ban Have to use `Object.assign` to preserve the original object.
Object.assign(event, data);
}

dispatchEvent(this.element, event);
await this._stabilize();
}

Expand Down
8 changes: 8 additions & 0 deletions src/cdk/testing/tests/cross-environment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,14 @@ export function crossEnvironmentSpecs(
expect(await target.text()).toBe('Basic event: 1');
});

it('should dispatch a custom event with attached data', async () => {
const target = await harness.customEventObject();

// @breaking-change 12.0.0 Remove non-null assertion once `dispatchEvent` is required.
await target.dispatchEvent!('myCustomEvent', {message: 'Hello', value: 1337});
expect(await target.text()).toBe('Event with object: {"message":"Hello","value":1337}');
});

it('should get TestElements and ComponentHarnesses', async () => {
const results = await harness.subcomponentHarnessesAndElements();
expect(results.length).toBe(5);
Expand Down
1 change: 1 addition & 0 deletions src/cdk/testing/tests/harnesses/main-component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export class MainComponentHarness extends ComponentHarness {
'test-shadow-boundary test-sub-shadow-boundary > .in-the-shadows');
readonly hoverTest = this.locatorFor('#hover-box');
readonly customEventBasic = this.locatorFor('#custom-event-basic');
readonly customEventObject = this.locatorFor('#custom-event-object');

private _testTools = this.locatorFor(SubComponentHarness);

Expand Down
1 change: 1 addition & 0 deletions src/cdk/testing/tests/test-main-component.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ <h1 style="height: 100px; width: 200px;">Main Component</h1>
<div id="multi-select-value">Multi-select: {{multiSelect}}</div>
<div id="multi-select-change-counter">Change events: {{multiSelectChangeEventCount}}</div>
<div (myCustomEvent)="basicEvent = basicEvent + 1" id="custom-event-basic">Basic event: {{basicEvent}}</div>
<div (myCustomEvent)="onCustomEvent($event)" id="custom-event-object">Event with object: {{customEventData}}</div>
</div>
<div class="subcomponents">
<test-sub class="test-special" title="test tools" [items]="testTools"></test-sub>
Expand Down
5 changes: 5 additions & 0 deletions src/cdk/testing/tests/test-main-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class TestMainComponent implements OnDestroy {
multiSelect: string[] = [];
multiSelectChangeEventCount = 0;
basicEvent = 0;
customEventData: string | null = null;
_shadowDomSupported = _supportsShadowDom();

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

onCustomEvent(event: any) {
this.customEventData = JSON.stringify({message: event.message, value: event.value});
}

runTaskOutsideZone() {
this._zone.runOutsideAngular(() => setTimeout(() => {
this.taskStateResultElement.nativeElement.textContent = 'result';
Expand Down
6 changes: 5 additions & 1 deletion tools/public_api_guard/cdk/testing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export interface ElementDimensions {
width: number;
}

export declare type EventData = string | number | boolean | undefined | null | EventData[] | {
[key: string]: EventData;
};

export declare function handleAutoChangeDetectionStatus(handler: (status: AutoChangeDetectionStatus) => void): void;

export declare abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFactory {
Expand Down Expand Up @@ -135,7 +139,7 @@ export interface TestElement {
click(): Promise<void>;
click(location: 'center'): Promise<void>;
click(relativeX: number, relativeY: number): Promise<void>;
dispatchEvent?(name: string): Promise<void>;
dispatchEvent?(name: string, data?: Record<string, EventData>): Promise<void>;
focus(): Promise<void>;
getAttribute(name: string): Promise<string | null>;
getCssValue(property: string): Promise<string>;
Expand Down
2 changes: 1 addition & 1 deletion tools/public_api_guard/cdk/testing/protractor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export declare class ProtractorElement implements TestElement {
blur(): Promise<void>;
clear(): Promise<void>;
click(...args: [] | ['center'] | [number, number]): Promise<void>;
dispatchEvent(name: string): Promise<void>;
dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void>;
focus(): Promise<void>;
getAttribute(name: string): Promise<string | null>;
getCssValue(property: string): Promise<string>;
Expand Down
2 changes: 1 addition & 1 deletion tools/public_api_guard/cdk/testing/testbed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export declare class UnitTestElement implements TestElement {
blur(): Promise<void>;
clear(): Promise<void>;
click(...args: [] | ['center'] | [number, number]): Promise<void>;
dispatchEvent(name: string): Promise<void>;
dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void>;
focus(): Promise<void>;
getAttribute(name: string): Promise<string | null>;
getCssValue(property: string): Promise<string>;
Expand Down