From c0ef982dc4145009b9c4a82f281f6b57a43034b3 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Mon, 24 Aug 2020 20:46:02 +0200 Subject: [PATCH] feat(cdk/testing): support right clicking on a TestElement Adds the ability to right click somewhere within a `TestElement`. Fixes #20385. --- .../testing/protractor/protractor-element.ts | 29 +++++++--- src/cdk/testing/test-element.ts | 8 +++ .../testbed/fake-events/dispatch-events.ts | 5 +- src/cdk/testing/testbed/unit-test-element.ts | 55 ++++++++++++------- .../testing/tests/cross-environment.spec.ts | 7 +++ .../tests/harnesses/main-component-harness.ts | 1 + .../testing/tests/test-main-component.html | 5 +- src/cdk/testing/tests/test-main-component.ts | 19 +++++-- tools/public_api_guard/cdk/testing.d.ts | 1 + .../cdk/testing/protractor.d.ts | 1 + .../public_api_guard/cdk/testing/testbed.d.ts | 1 + 11 files changed, 95 insertions(+), 37 deletions(-) diff --git a/src/cdk/testing/protractor/protractor-element.ts b/src/cdk/testing/protractor/protractor-element.ts index 14f6327ca97c..18b99b7e603e 100644 --- a/src/cdk/testing/protractor/protractor-element.ts +++ b/src/cdk/testing/protractor/protractor-element.ts @@ -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 = { @@ -81,15 +81,11 @@ export class ProtractorElement implements TestElement { } async click(...args: [] | ['center'] | [number, number]): Promise { - // 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 { + await this._dispatchClickEventSequence(args, Button.RIGHT); } async focus(): Promise { @@ -202,6 +198,21 @@ export class ProtractorElement implements TestElement { async dispatchEvent(name: string): Promise { 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(); + } } /** diff --git a/src/cdk/testing/test-element.ts b/src/cdk/testing/test-element.ts index bd5a3399589d..f46380085e40 100644 --- a/src/cdk/testing/test-element.ts +++ b/src/cdk/testing/test-element.ts @@ -84,6 +84,14 @@ export interface TestElement { */ click(relativeX: number, relativeY: number): Promise; + /** + * 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; + /** Focus the element. */ focus(): Promise; diff --git a/src/cdk/testing/testbed/fake-events/dispatch-events.ts b/src/cdk/testing/testbed/fake-events/dispatch-events.ts index 706c8ba17904..d920115a77cf 100644 --- a/src/cdk/testing/testbed/fake-events/dispatch-events.ts +++ b/src/cdk/testing/testbed/fake-events/dispatch-events.ts @@ -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)); } /** diff --git a/src/cdk/testing/testbed/unit-test-element.ts b/src/cdk/testing/testbed/unit-test-element.ts index 58384e110574..d586d1cf47ac 100644 --- a/src/cdk/testing/testbed/unit-test-element.ts +++ b/src/cdk/testing/testbed/unit-test-element.ts @@ -78,24 +78,12 @@ export class UnitTestElement implements TestElement { } async click(...args: [] | ['center'] | [number, number]): Promise { - 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 { + await this._dispatchMouseEventSequence('contextmenu', args, 2); await this._stabilize(); } @@ -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(); + } } diff --git a/src/cdk/testing/tests/cross-environment.spec.ts b/src/cdk/testing/tests/cross-environment.spec.ts index 4af8d8cd4fa8..dcd17ea980d8 100644 --- a/src/cdk/testing/tests/cross-environment.spec.ts +++ b/src/cdk/testing/tests/cross-environment.spec.ts @@ -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(); diff --git a/src/cdk/testing/tests/harnesses/main-component-harness.ts b/src/cdk/testing/tests/harnesses/main-component-harness.ts index 61d40bc49eeb..d6a7199d6a55 100644 --- a/src/cdk/testing/tests/harnesses/main-component-harness.ts +++ b/src/cdk/testing/tests/harnesses/main-component-harness.ts @@ -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 diff --git a/src/cdk/testing/tests/test-main-component.html b/src/cdk/testing/tests/test-main-component.html index 77e63b5db1b2..33d04895f978 100644 --- a/src/cdk/testing/tests/test-main-component.html +++ b/src/cdk/testing/tests/test-main-component.html @@ -1,8 +1,9 @@ -
-
{{relativeX}}-{{relativeY}}
+
{{clickResult.x}}-{{clickResult.y}}
+
{{rightClickResult.x}}-{{rightClickResult.y}}-{{rightClickResult.button}}

Main Component

Hello {{username}} from Angular 2!
diff --git a/src/cdk/testing/tests/test-main-component.ts b/src/cdk/testing/tests/test-main-component.ts index 8590ddccc921..8aa02d4dbc8d 100644 --- a/src/cdk/testing/tests/test-main-component.ts +++ b/src/cdk/testing/tests/test-main-component.ts @@ -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; @ViewChild('taskStateResult') taskStateResultElement: ElementRef; @@ -89,9 +89,12 @@ 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() { @@ -99,4 +102,10 @@ export class TestMainComponent implements OnDestroy { 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); + } } diff --git a/tools/public_api_guard/cdk/testing.d.ts b/tools/public_api_guard/cdk/testing.d.ts index 6419ae4b5300..591eb4ede39d 100644 --- a/tools/public_api_guard/cdk/testing.d.ts +++ b/tools/public_api_guard/cdk/testing.d.ts @@ -146,6 +146,7 @@ export interface TestElement { isFocused(): Promise; matchesSelector(selector: string): Promise; mouseAway(): Promise; + rightClick?(relativeX: number, relativeY: number): Promise; selectOptions?(...optionIndexes: number[]): Promise; sendKeys(...keys: (string | TestKey)[]): Promise; sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise; diff --git a/tools/public_api_guard/cdk/testing/protractor.d.ts b/tools/public_api_guard/cdk/testing/protractor.d.ts index fd8ee6ebcb52..27eb02eb0d44 100644 --- a/tools/public_api_guard/cdk/testing/protractor.d.ts +++ b/tools/public_api_guard/cdk/testing/protractor.d.ts @@ -15,6 +15,7 @@ export declare class ProtractorElement implements TestElement { isFocused(): Promise; matchesSelector(selector: string): Promise; mouseAway(): Promise; + rightClick(...args: [] | ['center'] | [number, number]): Promise; selectOptions(...optionIndexes: number[]): Promise; sendKeys(...keys: (string | TestKey)[]): Promise; sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise; diff --git a/tools/public_api_guard/cdk/testing/testbed.d.ts b/tools/public_api_guard/cdk/testing/testbed.d.ts index e4a24c73646e..f3acf1369d65 100644 --- a/tools/public_api_guard/cdk/testing/testbed.d.ts +++ b/tools/public_api_guard/cdk/testing/testbed.d.ts @@ -33,6 +33,7 @@ export declare class UnitTestElement implements TestElement { isFocused(): Promise; matchesSelector(selector: string): Promise; mouseAway(): Promise; + rightClick(...args: [] | ['center'] | [number, number]): Promise; selectOptions(...optionIndexes: number[]): Promise; sendKeys(...keys: (string | TestKey)[]): Promise; sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise;