Skip to content

feat(cdk/testing): support right clicking on a TestElement #20400

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 14, 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
29 changes: 20 additions & 9 deletions src/cdk/testing/protractor/protractor-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
TestKey,
TextOptions
} from '@angular/cdk/testing';
import {browser, by, ElementFinder, Key} from 'protractor';
import {browser, Button, by, ElementFinder, Key} from 'protractor';

/** Maps the `TestKey` constants to Protractor's `Key` constants. */
const keyMap = {
Expand Down Expand Up @@ -81,15 +81,11 @@ export class ProtractorElement implements TestElement {
}

async click(...args: [] | ['center'] | [number, number]): Promise<void> {
// Omitting the offset argument to mouseMove results in clicking the center.
// This is the default behavior we want, so we use an empty array of offsetArgs if no args are
// passed to this method.
const offsetArgs = args.length === 2 ? [{x: args[0], y: args[1]}] : [];
await this._dispatchClickEventSequence(args);
}

await browser.actions()
.mouseMove(await this.element.getWebElement(), ...offsetArgs)
.click()
.perform();
async rightClick(...args: [] | ['center'] | [number, number]): Promise<void> {
await this._dispatchClickEventSequence(args, Button.RIGHT);
}

async focus(): Promise<void> {
Expand Down Expand Up @@ -202,6 +198,21 @@ export class ProtractorElement implements TestElement {
async dispatchEvent(name: string): Promise<void> {
return browser.executeScript(_dispatchEvent, name, this.element);
}

/** Dispatches all the events that are part of a click event sequence. */
private async _dispatchClickEventSequence(
args: [] | ['center'] | [number, number],
button?: string) {
// Omitting the offset argument to mouseMove results in clicking the center.
// This is the default behavior we want, so we use an empty array of offsetArgs if no args are
// passed to this method.
const offsetArgs = args.length === 2 ? [{x: args[0], y: args[1]}] : [];

await browser.actions()
.mouseMove(await this.element.getWebElement(), ...offsetArgs)
.click(button)
.perform();
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/cdk/testing/test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ export interface TestElement {
*/
click(relativeX: number, relativeY: number): Promise<void>;

/**
* Right clicks on the element at the specified coordinates relative to the top-left of it.
* @param relativeX Coordinate within the element, along the X-axis at which to click.
* @param relativeY Coordinate within the element, along the Y-axis at which to click.
* @breaking-change 11.0.0 To become a required method.
*/
rightClick?(relativeX: number, relativeY: number): Promise<void>;

/** Focus the element. */
focus(): Promise<void>;

Expand Down
5 changes: 3 additions & 2 deletions src/cdk/testing/testbed/fake-events/dispatch-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ export function dispatchKeyboardEvent(node: Node, type: string, keyCode?: number
* Shorthand to dispatch a mouse event on the specified coordinates.
* @docs-private
*/
export function dispatchMouseEvent(node: Node, type: string, clientX = 0, clientY = 0): MouseEvent {
return dispatchEvent(node, createMouseEvent(type, clientX, clientY));
export function dispatchMouseEvent(
node: Node, type: string, clientX = 0, clientY = 0, button?: number): MouseEvent {
return dispatchEvent(node, createMouseEvent(type, clientX, clientY, button));
}

/**
Expand Down
55 changes: 36 additions & 19 deletions src/cdk/testing/testbed/unit-test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,12 @@ export class UnitTestElement implements TestElement {
}

async click(...args: [] | ['center'] | [number, number]): Promise<void> {
let clientX: number | undefined = undefined;
let clientY: number | undefined = undefined;
if (args.length) {
const {left, top, width, height} = await this.getDimensions();
const relativeX = args[0] === 'center' ? width / 2 : args[0];
const relativeY = args[0] === 'center' ? height / 2 : args[1];

// Round the computed click position as decimal pixels are not
// supported by mouse events and could lead to unexpected results.
clientX = Math.round(left + relativeX);
clientY = Math.round(top + relativeY);
}
await this._dispatchMouseEventSequence('click', args);
await this._stabilize();
}

this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY);
dispatchMouseEvent(this.element, 'mousedown', clientX, clientY);
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY);
dispatchMouseEvent(this.element, 'mouseup', clientX, clientY);
dispatchMouseEvent(this.element, 'click', clientX, clientY);
async rightClick(...args: [] | ['center'] | [number, number]): Promise<void> {
await this._dispatchMouseEventSequence('contextmenu', args, 2);
await this._stabilize();
}

Expand Down Expand Up @@ -210,14 +198,43 @@ export class UnitTestElement implements TestElement {
* @param name Name of the pointer event to be dispatched.
* @param clientX Coordinate of the user's pointer along the X axis.
* @param clientY Coordinate of the user's pointer along the Y axis.
* @param button Mouse button that should be pressed when dispatching the event.
*/
private _dispatchPointerEventIfSupported(name: string, clientX?: number, clientY?: number) {
private _dispatchPointerEventIfSupported(
name: string, clientX?: number, clientY?: number, button?: number) {
// The latest versions of all browsers we support have the new `PointerEvent` API.
// Though since we capture the two most recent versions of these browsers, we also
// need to support Safari 12 at time of writing. Safari 12 does not have support for this,
// so we need to conditionally create and dispatch these events based on feature detection.
if (typeof PointerEvent !== 'undefined' && PointerEvent) {
dispatchPointerEvent(this.element, name, clientX, clientY);
dispatchPointerEvent(this.element, name, clientX, clientY, {isPrimary: true, button});
}
}

/** Dispatches all the events that are part of a mouse event sequence. */
private async _dispatchMouseEventSequence(
name: string,
args: [] | ['center'] | [number, number],
button?: number) {
let clientX: number | undefined = undefined;
let clientY: number | undefined = undefined;

if (args.length) {
const {left, top, width, height} = await this.getDimensions();
const relativeX = args[0] === 'center' ? width / 2 : args[0];
const relativeY = args[0] === 'center' ? height / 2 : args[1];

// Round the computed click position as decimal pixels are not
// supported by mouse events and could lead to unexpected results.
clientX = Math.round(left + relativeX);
clientY = Math.round(top + relativeY);
}

this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY, button);
dispatchMouseEvent(this.element, 'mousedown', clientX, clientY, button);
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY, button);
dispatchMouseEvent(this.element, 'mouseup', clientX, clientY, button);
dispatchMouseEvent(this.element, name, clientX, clientY, button);
await this._stabilize();
}
}
7 changes: 7 additions & 0 deletions src/cdk/testing/tests/cross-environment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,13 @@ export function crossEnvironmentSpecs(
expect(await clickTestResult.text()).toBe('50-50');
});

it('should be able to right click at a specific position within an element', async () => {
const clickTest = await harness.clickTest();
const contextmenuTestResult = await harness.contextmenuTestResult();
await clickTest.rightClick!(50, 50);
expect(await contextmenuTestResult.text()).toBe('50-50-2');
});

it('should be able to send key', async () => {
const input = await harness.input();
const value = await harness.value();
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 @@ -34,6 +34,7 @@ export class MainComponentHarness extends ComponentHarness {
readonly multiSelect = this.locatorFor('#multi-select');
readonly multiSelectValue = this.locatorFor('#multi-select-value');
readonly multiSelectChangeEventCounter = this.locatorFor('#multi-select-change-counter');
readonly contextmenuTestResult = this.locatorFor('.contextmenu-test-result');
// Allow null for element
readonly nullItem = this.locatorForOptional('wrong locator');
// Allow null for component harness
Expand Down
5 changes: 3 additions & 2 deletions src/cdk/testing/tests/test-main-component.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<div class="click-test" (click)="onClick($event)"
<div class="click-test" (click)="onClick($event)" (contextmenu)="onRightClick($event)"
style="width: 100px; height: 100px; background: grey"
#clickTestElement>
</div>
<div class="click-test-result">{{relativeX}}-{{relativeY}}</div>
<div class="click-test-result">{{clickResult.x}}-{{clickResult.y}}</div>
<div class="contextmenu-test-result">{{rightClickResult.x}}-{{rightClickResult.y}}-{{rightClickResult.button}}</div>
<h1 style="height: 100px; width: 200px;">Main Component</h1>
<div id="username">Hello {{username}} from Angular 2!</div>
<div class="counters">
Expand Down
19 changes: 14 additions & 5 deletions src/cdk/testing/tests/test-main-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ export class TestMainComponent implements OnDestroy {
testMethods: string[];
isHovering = false;
specialKey = '';
relativeX = 0;
relativeY = 0;
singleSelect: string;
singleSelectChangeEventCount = 0;
multiSelect: string[] = [];
multiSelectChangeEventCount = 0;
basicEvent = 0;
_shadowDomSupported = _supportsShadowDom();
clickResult = {x: -1, y: -1};
rightClickResult = {x: -1, y: -1, button: -1};

@ViewChild('clickTestElement') clickTestElement: ElementRef<HTMLElement>;
@ViewChild('taskStateResult') taskStateResultElement: ElementRef<HTMLElement>;
Expand Down Expand Up @@ -89,14 +89,23 @@ export class TestMainComponent implements OnDestroy {
}

onClick(event: MouseEvent) {
const {top, left} = this.clickTestElement.nativeElement.getBoundingClientRect();
this.relativeX = Math.round(event.clientX - left);
this.relativeY = Math.round(event.clientY - top);
this._assignRelativeCoordinates(event, this.clickResult);
}

onRightClick(event: MouseEvent) {
this.rightClickResult.button = event.button;
this._assignRelativeCoordinates(event, this.rightClickResult);
}

runTaskOutsideZone() {
this._zone.runOutsideAngular(() => setTimeout(() => {
this.taskStateResultElement.nativeElement.textContent = 'result';
}, 100));
}

private _assignRelativeCoordinates(event: MouseEvent, obj: {x: number, y: number}) {
const {top, left} = this.clickTestElement.nativeElement.getBoundingClientRect();
obj.x = Math.round(event.clientX - left);
obj.y = Math.round(event.clientY - top);
}
}
1 change: 1 addition & 0 deletions tools/public_api_guard/cdk/testing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export interface TestElement {
isFocused(): Promise<boolean>;
matchesSelector(selector: string): Promise<boolean>;
mouseAway(): Promise<void>;
rightClick?(relativeX: number, relativeY: number): Promise<void>;
selectOptions?(...optionIndexes: number[]): Promise<void>;
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
Expand Down
1 change: 1 addition & 0 deletions tools/public_api_guard/cdk/testing/protractor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export declare class ProtractorElement implements TestElement {
isFocused(): Promise<boolean>;
matchesSelector(selector: string): Promise<boolean>;
mouseAway(): Promise<void>;
rightClick(...args: [] | ['center'] | [number, number]): Promise<void>;
selectOptions(...optionIndexes: number[]): Promise<void>;
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
Expand Down
1 change: 1 addition & 0 deletions tools/public_api_guard/cdk/testing/testbed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export declare class UnitTestElement implements TestElement {
isFocused(): Promise<boolean>;
matchesSelector(selector: string): Promise<boolean>;
mouseAway(): Promise<void>;
rightClick(...args: [] | ['center'] | [number, number]): Promise<void>;
selectOptions(...optionIndexes: number[]): Promise<void>;
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
Expand Down