diff --git a/src/cdk/testing/protractor/protractor-element.ts b/src/cdk/testing/protractor/protractor-element.ts index 9b016f48662d..a6ccd20e7476 100644 --- a/src/cdk/testing/protractor/protractor-element.ts +++ b/src/cdk/testing/protractor/protractor-element.ts @@ -81,11 +81,13 @@ export class ProtractorElement implements TestElement { return this.element.clear(); } - async click(...args: [] | ['center'] | [number, number]): Promise { - await this._dispatchClickEventSequence(args); + async click(...args: [ModifierKeys?] | ['center', ModifierKeys?] | + [number, number, ModifierKeys?]): Promise { + await this._dispatchClickEventSequence(args, Button.LEFT); } - async rightClick(...args: [] | ['center'] | [number, number]): Promise { + async rightClick(...args: [ModifierKeys?] | ['center', ModifierKeys?] | + [number, number, ModifierKeys?]): Promise { await this._dispatchClickEventSequence(args, Button.RIGHT); } @@ -202,17 +204,33 @@ export class ProtractorElement implements TestElement { /** Dispatches all the events that are part of a click event sequence. */ private async _dispatchClickEventSequence( - args: [] | ['center'] | [number, number], - button?: string) { + args: [ModifierKeys?] | ['center', ModifierKeys?] | + [number, number, ModifierKeys?], + button: string) { + let modifiers: ModifierKeys = {}; + if (args.length && typeof args[args.length - 1] === 'object') { + modifiers = args.pop() as ModifierKeys; + } + const modifierKeys = toProtractorModifierKeys(modifiers); + // 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(); + // This is the default behavior we want, so we use an empty array of offsetArgs if + // no args remain after popping the modifiers from the args passed to this function. + const offsetArgs = (args.length === 2 ? + [{x: args[0], y: args[1]}] : []) as [{x: number, y: number}]; + + let actions = browser.actions() + .mouseMove(await this.element.getWebElement(), ...offsetArgs); + + for (const modifierKey of modifierKeys) { + actions = actions.keyDown(modifierKey); + } + actions = actions.click(button); + for (const modifierKey of modifierKeys) { + actions = actions.keyUp(modifierKey); + } + + await actions.perform(); } } diff --git a/src/cdk/testing/test-element.ts b/src/cdk/testing/test-element.ts index 449ddb0fd33c..d5b0c1e0eab6 100644 --- a/src/cdk/testing/test-element.ts +++ b/src/cdk/testing/test-element.ts @@ -76,25 +76,27 @@ export interface TestElement { * the element is clicked at a specific location, consider using `click('center')` or * `click(x, y)` instead. */ - click(): Promise; + click(modifiers?: ModifierKeys): Promise; /** Click the element at the element's center. */ - click(location: 'center'): Promise; + click(location: 'center', modifiers?: ModifierKeys): Promise; /** * Click the element at the specified coordinates relative to the top-left of the element. * @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. + * @param modifiers Modifier keys held while clicking */ - click(relativeX: number, relativeY: number): Promise; + click(relativeX: number, relativeY: number, modifiers?: ModifierKeys): 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. + * @param modifiers Modifier keys held while clicking * @breaking-change 11.0.0 To become a required method. */ - rightClick?(relativeX: number, relativeY: number): Promise; + rightClick?(relativeX: number, relativeY: number, modifiers?: ModifierKeys): 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 d920115a77cf..ee4759ac0679 100644 --- a/src/cdk/testing/testbed/fake-events/dispatch-events.ts +++ b/src/cdk/testing/testbed/fake-events/dispatch-events.ts @@ -47,9 +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, button?: number): MouseEvent { - return dispatchEvent(node, createMouseEvent(type, clientX, clientY, button)); +export function dispatchMouseEvent( node: Node, type: string, clientX = 0, clientY = 0, + button?: number, modifiers?: ModifierKeys): MouseEvent { + return dispatchEvent(node, createMouseEvent(type, clientX, clientY, button, modifiers)); } /** diff --git a/src/cdk/testing/testbed/fake-events/event-objects.ts b/src/cdk/testing/testbed/fake-events/event-objects.ts index 331d5869eb94..f211df2920f6 100644 --- a/src/cdk/testing/testbed/fake-events/event-objects.ts +++ b/src/cdk/testing/testbed/fake-events/event-objects.ts @@ -12,7 +12,8 @@ import {ModifierKeys} from '@angular/cdk/testing'; * Creates a browser MouseEvent with the specified options. * @docs-private */ -export function createMouseEvent(type: string, clientX = 0, clientY = 0, button = 0) { +export function createMouseEvent( + type: string, clientX = 0, clientY = 0, button = 0, modifiers: ModifierKeys = {}) { const event = document.createEvent('MouseEvent'); const originalPreventDefault = event.preventDefault.bind(event); @@ -32,10 +33,10 @@ export function createMouseEvent(type: string, clientX = 0, clientY = 0, button /* screenY */ screenY, /* clientX */ clientX, /* clientY */ clientY, - /* ctrlKey */ false, - /* altKey */ false, - /* shiftKey */ false, - /* metaKey */ false, + /* ctrlKey */ !!modifiers.control, + /* altKey */ !!modifiers.alt, + /* shiftKey */ !!modifiers.shift, + /* metaKey */ !!modifiers.meta, /* button */ button, /* relatedTarget */ null); diff --git a/src/cdk/testing/testbed/unit-test-element.ts b/src/cdk/testing/testbed/unit-test-element.ts index 15f14e199c57..253cbb2a887a 100644 --- a/src/cdk/testing/testbed/unit-test-element.ts +++ b/src/cdk/testing/testbed/unit-test-element.ts @@ -80,12 +80,14 @@ export class UnitTestElement implements TestElement { await this._stabilize(); } - async click(...args: [] | ['center'] | [number, number]): Promise { - await this._dispatchMouseEventSequence('click', args); + async click(...args: [ModifierKeys?] | ['center', ModifierKeys?] | + [number, number, ModifierKeys?]): Promise { + await this._dispatchMouseEventSequence('click', args, 0); await this._stabilize(); } - async rightClick(...args: [] | ['center'] | [number, number]): Promise { + async rightClick(...args: [ModifierKeys?] | ['center', ModifierKeys?] | + [number, number, ModifierKeys?]): Promise { await this._dispatchMouseEventSequence('contextmenu', args, 2); await this._stabilize(); } @@ -224,15 +226,20 @@ export class UnitTestElement implements TestElement { /** Dispatches all the events that are part of a mouse event sequence. */ private async _dispatchMouseEventSequence( name: string, - args: [] | ['center'] | [number, number], + args: [ModifierKeys?] | ['center', ModifierKeys?] | [number, number, ModifierKeys?], button?: number) { let clientX: number | undefined = undefined; let clientY: number | undefined = undefined; + let modifiers: ModifierKeys = {}; + + if (args.length && typeof args[args.length - 1] === 'object') { + modifiers = args.pop() as ModifierKeys; + } 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]; + const relativeX = args[0] === 'center' ? width / 2 : args[0] as number; + const relativeY = args[0] === 'center' ? height / 2 : args[1] as number; // Round the computed click position as decimal pixels are not // supported by mouse events and could lead to unexpected results. @@ -241,10 +248,14 @@ export class UnitTestElement implements TestElement { } this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY, button); - dispatchMouseEvent(this.element, 'mousedown', clientX, clientY, button); + dispatchMouseEvent(this.element, 'mousedown', clientX, clientY, button, modifiers); this._dispatchPointerEventIfSupported('pointerup', clientX, clientY, button); - dispatchMouseEvent(this.element, 'mouseup', clientX, clientY, button); - dispatchMouseEvent(this.element, name, clientX, clientY, button); + dispatchMouseEvent(this.element, 'mouseup', clientX, clientY, button, modifiers); + dispatchMouseEvent(this.element, name, clientX, clientY, button, modifiers); + + // This call to _stabilize should not be needed since the callers will already do that them- + // selves. Nevertheless it breaks some tests in g3 without it. It needs to be investigated + // why removing breaks those tests. await this._stabilize(); } } diff --git a/src/cdk/testing/tests/cross-environment.spec.ts b/src/cdk/testing/tests/cross-environment.spec.ts index bd66c53aba6b..ff7b0a6d5c2a 100644 --- a/src/cdk/testing/tests/cross-environment.spec.ts +++ b/src/cdk/testing/tests/cross-environment.spec.ts @@ -320,6 +320,22 @@ export function crossEnvironmentSpecs( expect(await counter.text()).toBe('3'); }); + it('should be able to click with no modifiers', async () => { + const clickTest = await harness.clickTest(); + const modifiersResult = await harness.clickModifiersResult(); + + await clickTest.click(); + expect(await modifiersResult.text()).toBe('---'); + }); + + it('should be able to click with shift and meta modifiers', async () => { + const clickTest = await harness.clickTest(); + const modifiersResult = await harness.clickModifiersResult(); + + await clickTest.click({shift: true, meta: true}); + expect(await modifiersResult.text()).toBe('shift---meta'); + }); + it('should be able to click at a specific position within an element', async () => { const clickTest = await harness.clickTest(); const clickTestResult = await harness.clickTestResult(); @@ -327,6 +343,14 @@ export function crossEnvironmentSpecs( expect(await clickTestResult.text()).toBe('10-10'); }); + it('should be able to click at a specific position with shift and meta modifiers', async () => { + const clickTest = await harness.clickTest(); + const modifiersResult = await harness.clickModifiersResult(); + + await clickTest.click(10, 10, {shift: true, meta: true}); + expect(await modifiersResult.text()).toBe('shift---meta'); + }); + it('should be able to click the center of an element', async () => { const clickTest = await harness.clickTest(); const clickTestResult = await harness.clickTestResult(); @@ -334,6 +358,14 @@ export function crossEnvironmentSpecs( expect(await clickTestResult.text()).toBe('50-50'); }); + it('should be able to click the center of an element with shift meta modifiers', async () => { + const clickTest = await harness.clickTest(); + const modifiersResult = await harness.clickModifiersResult(); + + await clickTest.click('center', {shift: true, meta: true}); + expect(await modifiersResult.text()).toBe('shift---meta'); + }); + 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(); @@ -341,6 +373,13 @@ export function crossEnvironmentSpecs( expect(await contextmenuTestResult.text()).toBe('50-50-2'); }); + it('should be able to right click with modifiers', async () => { + const clickTest = await harness.clickTest(); + const modifiersResult = await harness.clickModifiersResult(); + await clickTest.rightClick!(50, 50, {alt: true, control: true}); + expect(await modifiersResult.text()).toBe('-alt-control-'); + }); + 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 815ea4b8ffbc..eef84e864823 100644 --- a/src/cdk/testing/tests/harnesses/main-component-harness.ts +++ b/src/cdk/testing/tests/harnesses/main-component-harness.ts @@ -28,6 +28,7 @@ export class MainComponentHarness extends ComponentHarness { readonly memo = this.locatorFor('textarea'); readonly clickTest = this.locatorFor('.click-test'); readonly clickTestResult = this.locatorFor('.click-test-result'); + readonly clickModifiersResult = this.locatorFor('.click-modifiers-test-result'); readonly singleSelect = this.locatorFor('#single-select'); readonly singleSelectValue = this.locatorFor('#single-select-value'); readonly singleSelectChangeEventCounter = this.locatorFor('#single-select-change-counter'); diff --git a/src/cdk/testing/tests/test-main-component.html b/src/cdk/testing/tests/test-main-component.html index 6ca01563039c..9e7c614c1321 100644 --- a/src/cdk/testing/tests/test-main-component.html +++ b/src/cdk/testing/tests/test-main-component.html @@ -4,6 +4,7 @@
{{clickResult.x}}-{{clickResult.y}}
{{rightClickResult.x}}-{{rightClickResult.y}}-{{rightClickResult.button}}
+
{{modifiers}}

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 85da6d6eef63..106728dd4731 100644 --- a/src/cdk/testing/tests/test-main-component.ts +++ b/src/cdk/testing/tests/test-main-component.ts @@ -35,6 +35,7 @@ export class TestMainComponent implements OnDestroy { testMethods: string[]; isHovering = false; specialKey = ''; + modifiers: string; singleSelect: string; singleSelectChangeEventCount = 0; multiSelect: string[] = []; @@ -91,11 +92,17 @@ export class TestMainComponent implements OnDestroy { onClick(event: MouseEvent) { this._assignRelativeCoordinates(event, this.clickResult); + + this.modifiers = ['Shift', 'Alt', 'Control', 'Meta'] + .map(key => event.getModifierState(key) ? key.toLowerCase() : '').join('-'); } onRightClick(event: MouseEvent) { this.rightClickResult.button = event.button; this._assignRelativeCoordinates(event, this.rightClickResult); + + this.modifiers = ['Shift', 'Alt', 'Control', 'Meta'] + .map(key => event.getModifierState(key) ? key.toLowerCase() : '').join('-'); } onCustomEvent(event: any) { diff --git a/tools/public_api_guard/cdk/testing.d.ts b/tools/public_api_guard/cdk/testing.d.ts index 0807ba8bb58f..0a4cd5b89cef 100644 --- a/tools/public_api_guard/cdk/testing.d.ts +++ b/tools/public_api_guard/cdk/testing.d.ts @@ -151,9 +151,9 @@ export declare function stopHandlingAutoChangeDetectionStatus(): void; export interface TestElement { blur(): Promise; clear(): Promise; - click(): Promise; - click(location: 'center'): Promise; - click(relativeX: number, relativeY: number): Promise; + click(modifiers?: ModifierKeys): Promise; + click(location: 'center', modifiers?: ModifierKeys): Promise; + click(relativeX: number, relativeY: number, modifiers?: ModifierKeys): Promise; dispatchEvent?(name: string, data?: Record): Promise; focus(): Promise; getAttribute(name: string): Promise; @@ -165,7 +165,7 @@ export interface TestElement { isFocused(): Promise; matchesSelector(selector: string): Promise; mouseAway(): Promise; - rightClick?(relativeX: number, relativeY: number): Promise; + rightClick?(relativeX: number, relativeY: number, modifiers?: ModifierKeys): 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 449a3e3f2563..9706421b9181 100644 --- a/tools/public_api_guard/cdk/testing/protractor.d.ts +++ b/tools/public_api_guard/cdk/testing/protractor.d.ts @@ -3,7 +3,11 @@ export declare class ProtractorElement implements TestElement { constructor(element: ElementFinder); blur(): Promise; clear(): Promise; - click(...args: [] | ['center'] | [number, number]): Promise; + click(...args: [ModifierKeys?] | ['center', ModifierKeys?] | [ + number, + number, + ModifierKeys? + ]): Promise; dispatchEvent(name: string, data?: Record): Promise; focus(): Promise; getAttribute(name: string): Promise; @@ -15,7 +19,11 @@ export declare class ProtractorElement implements TestElement { isFocused(): Promise; matchesSelector(selector: string): Promise; mouseAway(): Promise; - rightClick(...args: [] | ['center'] | [number, number]): Promise; + rightClick(...args: [ModifierKeys?] | ['center', ModifierKeys?] | [ + number, + number, + ModifierKeys? + ]): 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 7176691658c2..93a0430e9a0b 100644 --- a/tools/public_api_guard/cdk/testing/testbed.d.ts +++ b/tools/public_api_guard/cdk/testing/testbed.d.ts @@ -21,7 +21,11 @@ export declare class UnitTestElement implements TestElement { constructor(element: Element, _stabilize: () => Promise); blur(): Promise; clear(): Promise; - click(...args: [] | ['center'] | [number, number]): Promise; + click(...args: [ModifierKeys?] | ['center', ModifierKeys?] | [ + number, + number, + ModifierKeys? + ]): Promise; dispatchEvent(name: string, data?: Record): Promise; focus(): Promise; getAttribute(name: string): Promise; @@ -33,7 +37,11 @@ export declare class UnitTestElement implements TestElement { isFocused(): Promise; matchesSelector(selector: string): Promise; mouseAway(): Promise; - rightClick(...args: [] | ['center'] | [number, number]): Promise; + rightClick(...args: [ModifierKeys?] | ['center', ModifierKeys?] | [ + number, + number, + ModifierKeys? + ]): Promise; selectOptions(...optionIndexes: number[]): Promise; sendKeys(...keys: (string | TestKey)[]): Promise; sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise;