Skip to content

Commit b1be164

Browse files
authored
feat(cdk/testing): support right clicking on a TestElement (#20400)
Adds the ability to right click somewhere within a `TestElement`. Fixes #20385.
1 parent 483c3e2 commit b1be164

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> {
@@ -202,6 +198,21 @@ export class ProtractorElement implements TestElement {
202198
async dispatchEvent(name: string): Promise<void> {
203199
return browser.executeScript(_dispatchEvent, name, this.element);
204200
}
201+
202+
/** Dispatches all the events that are part of a click event sequence. */
203+
private async _dispatchClickEventSequence(
204+
args: [] | ['center'] | [number, number],
205+
button?: string) {
206+
// Omitting the offset argument to mouseMove results in clicking the center.
207+
// This is the default behavior we want, so we use an empty array of offsetArgs if no args are
208+
// passed to this method.
209+
const offsetArgs = args.length === 2 ? [{x: args[0], y: args[1]}] : [];
210+
211+
await browser.actions()
212+
.mouseMove(await this.element.getWebElement(), ...offsetArgs)
213+
.click(button)
214+
.perform();
215+
}
205216
}
206217

207218
/**

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

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

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
@@ -35,14 +35,14 @@ export class TestMainComponent implements OnDestroy {
3535
testMethods: string[];
3636
isHovering = false;
3737
specialKey = '';
38-
relativeX = 0;
39-
relativeY = 0;
4038
singleSelect: string;
4139
singleSelectChangeEventCount = 0;
4240
multiSelect: string[] = [];
4341
multiSelectChangeEventCount = 0;
4442
basicEvent = 0;
4543
_shadowDomSupported = _supportsShadowDom();
44+
clickResult = {x: -1, y: -1};
45+
rightClickResult = {x: -1, y: -1, button: -1};
4646

4747
@ViewChild('clickTestElement') clickTestElement: ElementRef<HTMLElement>;
4848
@ViewChild('taskStateResult') taskStateResultElement: ElementRef<HTMLElement>;
@@ -89,14 +89,23 @@ export class TestMainComponent implements OnDestroy {
8989
}
9090

9191
onClick(event: MouseEvent) {
92-
const {top, left} = this.clickTestElement.nativeElement.getBoundingClientRect();
93-
this.relativeX = Math.round(event.clientX - left);
94-
this.relativeY = Math.round(event.clientY - top);
92+
this._assignRelativeCoordinates(event, this.clickResult);
93+
}
94+
95+
onRightClick(event: MouseEvent) {
96+
this.rightClickResult.button = event.button;
97+
this._assignRelativeCoordinates(event, this.rightClickResult);
9598
}
9699

97100
runTaskOutsideZone() {
98101
this._zone.runOutsideAngular(() => setTimeout(() => {
99102
this.taskStateResultElement.nativeElement.textContent = 'result';
100103
}, 100));
101104
}
105+
106+
private _assignRelativeCoordinates(event: MouseEvent, obj: {x: number, y: number}) {
107+
const {top, left} = this.clickTestElement.nativeElement.getBoundingClientRect();
108+
obj.x = Math.round(event.clientX - left);
109+
obj.y = Math.round(event.clientY - top);
110+
}
102111
}

tools/public_api_guard/cdk/testing.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export interface TestElement {
146146
isFocused(): Promise<boolean>;
147147
matchesSelector(selector: string): Promise<boolean>;
148148
mouseAway(): Promise<void>;
149+
rightClick?(relativeX: number, relativeY: number): Promise<void>;
149150
selectOptions?(...optionIndexes: number[]): Promise<void>;
150151
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
151152
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
@@ -15,6 +15,7 @@ export declare class ProtractorElement implements TestElement {
1515
isFocused(): Promise<boolean>;
1616
matchesSelector(selector: string): Promise<boolean>;
1717
mouseAway(): Promise<void>;
18+
rightClick(...args: [] | ['center'] | [number, number]): Promise<void>;
1819
selectOptions(...optionIndexes: number[]): Promise<void>;
1920
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
2021
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
@@ -33,6 +33,7 @@ export declare class UnitTestElement implements TestElement {
3333
isFocused(): Promise<boolean>;
3434
matchesSelector(selector: string): Promise<boolean>;
3535
mouseAway(): Promise<void>;
36+
rightClick(...args: [] | ['center'] | [number, number]): Promise<void>;
3637
selectOptions(...optionIndexes: number[]): Promise<void>;
3738
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
3839
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;

0 commit comments

Comments
 (0)