Skip to content

Commit e4a6b20

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 4ef3d3f commit e4a6b20

File tree

11 files changed

+95
-37
lines changed

11 files changed

+95
-37
lines changed

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

Lines changed: 20 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, by, ElementFinder, Key} from 'protractor';
17+
import {browser, Button, by, ElementFinder, Key} 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: [] | ['center'] | [number, 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 === 2 ? [{x: args[0], y: args[1]}] : [];
84+
await this._dispatchClickEventSequence(args);
85+
}
8886

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

9591
async focus(): Promise<void> {
@@ -198,4 +194,19 @@ export class ProtractorElement implements TestElement {
198194
async isFocused(): Promise<boolean> {
199195
return this.element.equals(browser.driver.switchTo().activeElement());
200196
}
197+
198+
/** Dispatches all the events that are part of a click event sequence. */
199+
private async _dispatchClickEventSequence(
200+
args: [] | ['center'] | [number, number],
201+
button?: string) {
202+
// Omitting the offset argument to mouseMove results in clicking the center.
203+
// This is the default behavior we want, so we use an empty array of offsetArgs if no args are
204+
// passed to this method.
205+
const offsetArgs = args.length === 2 ? [{x: args[0], y: args[1]}] : [];
206+
207+
await browser.actions()
208+
.mouseMove(await this.element.getWebElement(), ...offsetArgs)
209+
.click(button)
210+
.perform();
211+
}
201212
}

src/cdk/testing/test-element.ts

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

87+
/**
88+
* Right clicks on the element at the specified coordinates relative to the top-left of it.
89+
* @param relativeX Coordinate within the element, along the X-axis at which to click.
90+
* @param relativeY Coordinate within the element, along the Y-axis at which to click.
91+
* @breaking-change 11.0.0 To become a required method.
92+
*/
93+
rightClick?(relativeX: number, relativeY: number): Promise<void>;
94+
8795
/** Focus the element. */
8896
focus(): Promise<void>;
8997

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: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,12 @@ export class UnitTestElement implements TestElement {
7878
}
7979

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

94-
this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY);
95-
dispatchMouseEvent(this.element, 'mousedown', clientX, clientY);
96-
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY);
97-
dispatchMouseEvent(this.element, 'mouseup', clientX, clientY);
98-
dispatchMouseEvent(this.element, 'click', clientX, clientY);
85+
async rightClick(...args: [] | ['center'] | [number, number]): Promise<void> {
86+
await this._dispatchMouseEventSequence('contextmenu', args, 2);
9987
await this._stabilize();
10088
}
10189

@@ -205,14 +193,43 @@ export class UnitTestElement implements TestElement {
205193
* @param name Name of the pointer event to be dispatched.
206194
* @param clientX Coordinate of the user's pointer along the X axis.
207195
* @param clientY Coordinate of the user's pointer along the Y axis.
196+
* @param button Mouse button that should be pressed when dispatching the event.
208197
*/
209-
private _dispatchPointerEventIfSupported(name: string, clientX?: number, clientY?: number) {
198+
private _dispatchPointerEventIfSupported(
199+
name: string, clientX?: number, clientY?: number, button?: number) {
210200
// The latest versions of all browsers we support have the new `PointerEvent` API.
211201
// Though since we capture the two most recent versions of these browsers, we also
212202
// need to support Safari 12 at time of writing. Safari 12 does not have support for this,
213203
// so we need to conditionally create and dispatch these events based on feature detection.
214204
if (typeof PointerEvent !== 'undefined' && PointerEvent) {
215-
dispatchPointerEvent(this.element, name, clientX, clientY);
205+
dispatchPointerEvent(this.element, name, clientX, clientY, {isPrimary: true, button});
216206
}
217207
}
208+
209+
/** Dispatches all the events that are part of a mouse event sequence. */
210+
private async _dispatchMouseEventSequence(
211+
name: string,
212+
args: [] | ['center'] | [number, number],
213+
button?: number) {
214+
let clientX: number | undefined = undefined;
215+
let clientY: number | undefined = undefined;
216+
217+
if (args.length) {
218+
const {left, top, width, height} = await this.getDimensions();
219+
const relativeX = args[0] === 'center' ? width / 2 : args[0];
220+
const relativeY = args[0] === 'center' ? height / 2 : args[1];
221+
222+
// Round the computed click position as decimal pixels are not
223+
// supported by mouse events and could lead to unexpected results.
224+
clientX = Math.round(left + relativeX);
225+
clientY = Math.round(top + relativeY);
226+
}
227+
228+
this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY, button);
229+
dispatchMouseEvent(this.element, 'mousedown', clientX, clientY, button);
230+
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY, button);
231+
dispatchMouseEvent(this.element, 'mouseup', clientX, clientY, button);
232+
dispatchMouseEvent(this.element, name, clientX, clientY, button);
233+
await this._stabilize();
234+
}
218235
}

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

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

336+
it('should be able to right click at a specific position within an element', async () => {
337+
const clickTest = await harness.clickTest();
338+
const contextmenuTestResult = await harness.contextmenuTestResult();
339+
await clickTest.rightClick!(50, 50);
340+
expect(await contextmenuTestResult.text()).toBe('50-50-2');
341+
});
342+
336343
it('should be able to send key', async () => {
337344
const input = await harness.input();
338345
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
@@ -34,6 +34,7 @@ export class MainComponentHarness extends ComponentHarness {
3434
readonly multiSelect = this.locatorFor('#multi-select');
3535
readonly multiSelectValue = this.locatorFor('#multi-select-value');
3636
readonly multiSelectChangeEventCounter = this.locatorFor('#multi-select-change-counter');
37+
readonly contextmenuTestResult = this.locatorFor('.contextmenu-test-result');
3738
// Allow null for element
3839
readonly nullItem = this.locatorForOptional('wrong locator');
3940
// 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,13 +41,13 @@ export class TestMainComponent implements OnDestroy {
4141
testMethods: string[];
4242
_isHovering: boolean;
4343
specialKey = '';
44-
relativeX = 0;
45-
relativeY = 0;
4644
singleSelect: string;
4745
singleSelectChangeEventCount = 0;
4846
multiSelect: string[] = [];
4947
multiSelectChangeEventCount = 0;
5048
_shadowDomSupported = _supportsShadowDom();
49+
clickResult = {x: -1, y: -1};
50+
rightClickResult = {x: -1, y: -1, button: -1};
5151

5252
@ViewChild('clickTestElement') clickTestElement: ElementRef<HTMLElement>;
5353
@ViewChild('taskStateResult') taskStateResultElement: ElementRef<HTMLElement>;
@@ -102,14 +102,23 @@ export class TestMainComponent implements OnDestroy {
102102
}
103103

104104
onClick(event: MouseEvent) {
105-
const {top, left} = this.clickTestElement.nativeElement.getBoundingClientRect();
106-
this.relativeX = Math.round(event.clientX - left);
107-
this.relativeY = Math.round(event.clientY - top);
105+
this._assignRelativeCoordinates(event, this.clickResult);
106+
}
107+
108+
onRightClick(event: MouseEvent) {
109+
this.rightClickResult.button = event.button;
110+
this._assignRelativeCoordinates(event, this.rightClickResult);
108111
}
109112

110113
runTaskOutsideZone() {
111114
this._zone.runOutsideAngular(() => setTimeout(() => {
112115
this.taskStateResultElement.nativeElement.textContent = 'result';
113116
}, 100));
114117
}
118+
119+
private _assignRelativeCoordinates(event: MouseEvent, obj: {x: number, y: number}) {
120+
const {top, left} = this.clickTestElement.nativeElement.getBoundingClientRect();
121+
obj.x = Math.round(event.clientX - left);
122+
obj.y = Math.round(event.clientY - top);
123+
}
115124
}

tools/public_api_guard/cdk/testing.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export interface TestElement {
144144
isFocused(): Promise<boolean>;
145145
matchesSelector(selector: string): Promise<boolean>;
146146
mouseAway(): Promise<void>;
147+
rightClick?(relativeX: number, relativeY: number): Promise<void>;
147148
selectOptions?(...optionIndexes: number[]): Promise<void>;
148149
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
149150
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): 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: [] | ['center'] | [number, number]): Promise<void>;
1718
selectOptions(...optionIndexes: number[]): Promise<void>;
1819
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
1920
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): 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
@@ -32,6 +32,7 @@ export declare class UnitTestElement implements TestElement {
3232
isFocused(): Promise<boolean>;
3333
matchesSelector(selector: string): Promise<boolean>;
3434
mouseAway(): Promise<void>;
35+
rightClick(...args: [] | ['center'] | [number, number]): Promise<void>;
3536
selectOptions(...optionIndexes: number[]): Promise<void>;
3637
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
3738
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;

0 commit comments

Comments
 (0)