Skip to content

Commit 4d6d440

Browse files
committed
feat(cdk/testing): support right clicking on a TestElement
Adds the ability to right click somewhere within a `TestElement`. Fixes #20385.
1 parent f00f46a commit 4d6d440

File tree

11 files changed

+95
-33
lines changed

11 files changed

+95
-33
lines changed

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
TestKey,
1515
TextOptions
1616
} from '@angular/cdk/testing';
17-
import {browser, ElementFinder, Key} from 'protractor';
17+
import {browser, ElementFinder, Key, Button} from 'protractor';
1818

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

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

89-
await browser.actions()
90-
.mouseMove(await this.element.getWebElement(), ...offsetArgs)
91-
.click()
92-
.perform();
87+
async rightClick(...args: number[]): Promise<void> {
88+
await this._dispatchClickEventSequence(args[0], args[1], Button.RIGHT);
9389
}
9490

9591
async focus(): Promise<void> {
@@ -177,4 +173,22 @@ export class ProtractorElement implements TestElement {
177173
async isFocused(): Promise<boolean> {
178174
return this.element.equals(browser.driver.switchTo().activeElement());
179175
}
176+
177+
/**
178+
* Dispatches all the events that are part of a click event sequence.
179+
* @param x X coordinate of the event within the element.
180+
* @param y Y coordinate of the event within the element.
181+
* @param button Mouse button that should be pressed during the event.
182+
*/
183+
private async _dispatchClickEventSequence(x?: number, y?: number, button?: string) {
184+
// Omitting the offset argument to mouseMove results in clicking the center.
185+
// This is the default behavior we want, so we use an empty array of offsetArgs if no args are
186+
// passed to this method.
187+
const offsetArgs = (x != null && y != null) ? [{x, y}] : [];
188+
189+
await browser.actions()
190+
.mouseMove(await this.element.getWebElement(), ...offsetArgs)
191+
.click(button)
192+
.perform();
193+
}
180194
}

src/cdk/testing/test-element.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ export interface TestElement {
7777
*/
7878
click(relativeX: number, relativeY: number): Promise<void>;
7979

80+
/**
81+
* Right clicks on the element at the specified coordinates relative to the top-left of it.
82+
* @param relativeX Coordinate within the element, along the X-axis at which to click.
83+
* @param relativeY Coordinate within the element, along the Y-axis at which to click.
84+
* @breaking-change 11.0.0 To become a required method.
85+
*/
86+
rightClick?(relativeX: number, relativeY: number): Promise<void>;
87+
8088
/** Focus the element. */
8189
focus(): Promise<void>;
8290

src/cdk/testing/testbed/fake-events/dispatch-events.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ export function dispatchKeyboardEvent(node: Node, type: string, keyCode?: number
4747
* Shorthand to dispatch a mouse event on the specified coordinates.
4848
* @docs-private
4949
*/
50-
export function dispatchMouseEvent(node: Node, type: string, clientX = 0, clientY = 0): MouseEvent {
51-
return dispatchEvent(node, createMouseEvent(type, clientX, clientY));
50+
export function dispatchMouseEvent(
51+
node: Node, type: string, clientX = 0, clientY = 0, button?: number): MouseEvent {
52+
return dispatchEvent(node, createMouseEvent(type, clientX, clientY, button));
5253
}
5354

5455
/**

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

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,12 @@ export class UnitTestElement implements TestElement {
7777
}
7878

7979
async click(...args: number[]): Promise<void> {
80-
const {left, top, width, height} = await this.getDimensions();
81-
const relativeX = args.length ? args[0] : width / 2;
82-
const relativeY = args.length ? args[1] : height / 2;
83-
84-
// Round the computed click position as decimal pixels are not
85-
// supported by mouse events and could lead to unexpected results.
86-
const clientX = Math.round(left + relativeX);
87-
const clientY = Math.round(top + relativeY);
80+
await this._dispatchMouseEventSequence('click', args[0], args[1]);
81+
await this._stabilize();
82+
}
8883

89-
this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY);
90-
dispatchMouseEvent(this.element, 'mousedown', clientX, clientY);
91-
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY);
92-
dispatchMouseEvent(this.element, 'mouseup', clientX, clientY);
93-
dispatchMouseEvent(this.element, 'click', clientX, clientY);
84+
async rightClick(...args: number[]): Promise<void> {
85+
await this._dispatchMouseEventSequence('contextmenu', args[0], args[1], 2);
9486
await this._stabilize();
9587
}
9688

@@ -176,14 +168,40 @@ export class UnitTestElement implements TestElement {
176168
* @param name Name of the pointer event to be dispatched.
177169
* @param clientX Coordinate of the user's pointer along the X axis.
178170
* @param clientY Coordinate of the user's pointer along the Y axis.
171+
* @param button Mouse button that should be pressed when dispatching the event.
179172
*/
180-
private _dispatchPointerEventIfSupported(name: string, clientX?: number, clientY?: number) {
173+
private _dispatchPointerEventIfSupported(
174+
name: string, clientX?: number, clientY?: number, button?: number) {
181175
// The latest versions of all browsers we support have the new `PointerEvent` API.
182176
// Though since we capture the two most recent versions of these browsers, we also
183177
// need to support Safari 12 at time of writing. Safari 12 does not have support for this,
184178
// so we need to conditionally create and dispatch these events based on feature detection.
185179
if (typeof PointerEvent !== 'undefined' && PointerEvent) {
186-
dispatchPointerEvent(this.element, name, clientX, clientY);
180+
dispatchPointerEvent(this.element, name, clientX, clientY, {isPrimary: true, button});
187181
}
188182
}
183+
184+
/**
185+
* Dispatches all the events that are part of a mouse event sequence.
186+
* @param name Name of the final event in the sequence.
187+
* @param x X coordinate of the event within the element.
188+
* @param y Y coordinate of the event within the element.
189+
* @param button Mouse button that should be pressed during the event.
190+
*/
191+
private async _dispatchMouseEventSequence(name: string, x?: number, y?: number, button?: number) {
192+
const {left, top, width, height} = await this.getDimensions();
193+
const relativeX = x == null ? width / 2 : x;
194+
const relativeY = y == null ? height / 2 : y;
195+
196+
// Round the computed click position as decimal pixels are not
197+
// supported by mouse events and could lead to unexpected results.
198+
const clientX = Math.round(left + relativeX);
199+
const clientY = Math.round(top + relativeY);
200+
201+
this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY, button);
202+
dispatchMouseEvent(this.element, 'mousedown', clientX, clientY, button);
203+
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY, button);
204+
dispatchMouseEvent(this.element, 'mouseup', clientX, clientY, button);
205+
dispatchMouseEvent(this.element, name, clientX, clientY, button);
206+
}
189207
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,13 @@ export function crossEnvironmentSpecs(
326326
expect(await clickTestResult.text()).toBe('50-50');
327327
});
328328

329+
it('should be able to right click at a specific position within an element', async () => {
330+
const clickTest = await harness.clickTest();
331+
const contextmenuTestResult = await harness.contextmenuTestResult();
332+
await clickTest.rightClick!(50, 50);
333+
expect(await contextmenuTestResult.text()).toBe('50-50-2');
334+
});
335+
329336
it('should be able to send key', async () => {
330337
const input = await harness.input();
331338
const value = await harness.value();

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class MainComponentHarness extends ComponentHarness {
2828
readonly memo = this.locatorFor('textarea');
2929
readonly clickTest = this.locatorFor('.click-test');
3030
readonly clickTestResult = this.locatorFor('.click-test-result');
31+
readonly contextmenuTestResult = this.locatorFor('.contextmenu-test-result');
3132
// Allow null for element
3233
readonly nullItem = this.locatorForOptional('wrong locator');
3334
// Allow null for component harness

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
<div class="click-test" (click)="onClick($event)"
1+
<div class="click-test" (click)="onClick($event)" (contextmenu)="onRightClick($event)"
22
style="width: 100px; height: 100px; background: grey"
33
#clickTestElement>
44
</div>
5-
<div class="click-test-result">{{relativeX}}-{{relativeY}}</div>
5+
<div class="click-test-result">{{clickResult.x}}-{{clickResult.y}}</div>
6+
<div class="contextmenu-test-result">{{rightClickResult.x}}-{{rightClickResult.y}}-{{rightClickResult.button}}</div>
67
<h1 style="height: 100px; width: 200px;">Main Component</h1>
78
<div id="username">Hello {{username}} from Angular 2!</div>
89
<div class="counters">

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ export class TestMainComponent implements OnDestroy {
4141
testMethods: string[];
4242
_isHovering: boolean;
4343
specialKey = '';
44-
relativeX = 0;
45-
relativeY = 0;
4644
_shadowDomSupported = _supportsShadowDom();
45+
clickResult = {x: -1, y: -1};
46+
rightClickResult = {x: -1, y: -1, button: -1};
4747

4848
@ViewChild('clickTestElement') clickTestElement: ElementRef<HTMLElement>;
4949
@ViewChild('taskStateResult') taskStateResultElement: ElementRef<HTMLElement>;
@@ -98,14 +98,23 @@ export class TestMainComponent implements OnDestroy {
9898
}
9999

100100
onClick(event: MouseEvent) {
101-
const {top, left} = this.clickTestElement.nativeElement.getBoundingClientRect();
102-
this.relativeX = Math.round(event.clientX - left);
103-
this.relativeY = Math.round(event.clientY - top);
101+
this._assignRelativeCoordinates(event, this.clickResult);
102+
}
103+
104+
onRightClick(event: MouseEvent) {
105+
this.rightClickResult.button = event.button;
106+
this._assignRelativeCoordinates(event, this.rightClickResult);
104107
}
105108

106109
runTaskOutsideZone() {
107110
this._zone.runOutsideAngular(() => setTimeout(() => {
108111
this.taskStateResultElement.nativeElement.textContent = 'result';
109112
}, 100));
110113
}
114+
115+
private _assignRelativeCoordinates(event: MouseEvent, obj: {x: number, y: number}) {
116+
const {top, left} = this.clickTestElement.nativeElement.getBoundingClientRect();
117+
obj.x = Math.round(event.clientX - left);
118+
obj.y = Math.round(event.clientY - top);
119+
}
111120
}

tools/public_api_guard/cdk/testing.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export interface TestElement {
130130
isFocused(): Promise<boolean>;
131131
matchesSelector(selector: string): Promise<boolean>;
132132
mouseAway(): Promise<void>;
133+
rightClick?(relativeX: number, relativeY: number): Promise<void>;
133134
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
134135
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
135136
setInputValue?(value: string): Promise<void>;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export declare class ProtractorElement implements TestElement {
1414
isFocused(): Promise<boolean>;
1515
matchesSelector(selector: string): Promise<boolean>;
1616
mouseAway(): Promise<void>;
17+
rightClick(...args: number[]): Promise<void>;
1718
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
1819
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
1920
setInputValue(value: string): Promise<void>;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export declare class UnitTestElement implements TestElement {
3131
isFocused(): Promise<boolean>;
3232
matchesSelector(selector: string): Promise<boolean>;
3333
mouseAway(): Promise<void>;
34+
rightClick(...args: number[]): Promise<void>;
3435
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
3536
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
3637
setInputValue(value: string): Promise<void>;

0 commit comments

Comments
 (0)