Skip to content

Commit a2e12ec

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 f0c7a25 commit a2e12ec

File tree

11 files changed

+92
-33
lines changed

11 files changed

+92
-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 & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,21 +79,12 @@ export class UnitTestElement implements TestElement {
7979
}
8080

8181
async click(...args: number[]): Promise<void> {
82-
const {left, top, width, height} = await this.getDimensions();
83-
const relativeX = args.length ? args[0] : width / 2;
84-
const relativeY = args.length ? args[1] : height / 2;
85-
86-
// Round the computed click position as decimal pixels are not
87-
// supported by mouse events and could lead to unexpected results.
88-
const clientX = Math.round(left + relativeX);
89-
const clientY = Math.round(top + relativeY);
90-
91-
this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY);
92-
dispatchMouseEvent(this.element, 'mousedown', clientX, clientY);
93-
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY);
94-
dispatchMouseEvent(this.element, 'mouseup', clientX, clientY);
95-
dispatchMouseEvent(this.element, 'click', clientX, clientY);
82+
await this._dispatchMouseEventSequence('click', args[0], args[1]);
83+
await this._stabilize();
84+
}
9685

86+
async rightClick(...args: number[]): Promise<void> {
87+
await this._dispatchMouseEventSequence('contextmenu', args[0], args[1], 2);
9788
await this._stabilize();
9889
}
9990

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

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');
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)="onClick($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">{{clickResults.click.x}}-{{clickResults.click.y}}</div>
6+
<div class="contextmenu-test-result">{{clickResults.contextmenu.x}}-{{clickResults.contextmenu.y}}</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: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ export class TestMainComponent implements OnDestroy {
4141
testMethods: string[];
4242
_isHovering: boolean;
4343
specialKey = '';
44-
relativeX = 0;
45-
relativeY = 0;
4644
_shadowDomSupported = _supportsShadowDom();
45+
clickResults = {
46+
click: {x: -1, y: -1},
47+
contextmenu: {x: -1, y: -1}
48+
};
4749

4850
@ViewChild('clickTestElement') clickTestElement: ElementRef<HTMLElement>;
4951
@ViewChild('taskStateResult') taskStateResultElement: ElementRef<HTMLElement>;
@@ -98,9 +100,14 @@ export class TestMainComponent implements OnDestroy {
98100
}
99101

100102
onClick(event: MouseEvent) {
103+
if (event.type !== 'click' && event.type !== 'contextmenu') {
104+
throw Error(`Unknown click event ${event.type}`);
105+
}
106+
101107
const {top, left} = this.clickTestElement.nativeElement.getBoundingClientRect();
102-
this.relativeX = Math.round(event.clientX - left);
103-
this.relativeY = Math.round(event.clientY - top);
108+
const result = this.clickResults[event.type];
109+
result.x = Math.round(event.clientX - left);
110+
result.y = Math.round(event.clientY - top);
104111
}
105112

106113
runTaskOutsideZone() {

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)