Skip to content

feat(cdk/testing): support modifiers for clicking on a TestElement #20758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions src/cdk/testing/protractor/protractor-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ export class ProtractorElement implements TestElement {
return this.element.clear();
}

async click(...args: [] | ['center'] | [number, number]): Promise<void> {
await this._dispatchClickEventSequence(args);
async click(...args: [ModifierKeys?] | ['center', ModifierKeys?] |
[number, number, ModifierKeys?]): Promise<void> {
await this._dispatchClickEventSequence(args, Button.LEFT);
}

async rightClick(...args: [] | ['center'] | [number, number]): Promise<void> {
async rightClick(...args: [ModifierKeys?] | ['center', ModifierKeys?] |
[number, number, ModifierKeys?]): Promise<void> {
await this._dispatchClickEventSequence(args, Button.RIGHT);
}

Expand Down Expand Up @@ -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();
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/cdk/testing/test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
click(modifiers?: ModifierKeys): Promise<void>;

/** Click the element at the element's center. */
click(location: 'center'): Promise<void>;
click(location: 'center', modifiers?: ModifierKeys): Promise<void>;

/**
* 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<void>;
click(relativeX: number, relativeY: number, modifiers?: ModifierKeys): Promise<void>;

/**
* 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<void>;
rightClick?(relativeX: number, relativeY: number, modifiers?: ModifierKeys): Promise<void>;

/** Focus the element. */
focus(): Promise<void>;
Expand Down
6 changes: 3 additions & 3 deletions src/cdk/testing/testbed/fake-events/dispatch-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand Down
11 changes: 6 additions & 5 deletions src/cdk/testing/testbed/fake-events/event-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand Down
29 changes: 20 additions & 9 deletions src/cdk/testing/testbed/unit-test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ export class UnitTestElement implements TestElement {
await this._stabilize();
}

async click(...args: [] | ['center'] | [number, number]): Promise<void> {
await this._dispatchMouseEventSequence('click', args);
async click(...args: [ModifierKeys?] | ['center', ModifierKeys?] |
[number, number, ModifierKeys?]): Promise<void> {
await this._dispatchMouseEventSequence('click', args, 0);
await this._stabilize();
}

async rightClick(...args: [] | ['center'] | [number, number]): Promise<void> {
async rightClick(...args: [ModifierKeys?] | ['center', ModifierKeys?] |
[number, number, ModifierKeys?]): Promise<void> {
await this._dispatchMouseEventSequence('contextmenu', args, 2);
await this._stabilize();
}
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was await this._stabilize() removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_stabilize is already called from the public entry points click and rightClick that use this private helper. It's not necessary to call it more often.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a second await this._stabilize() to click to maintain the same behavior that it has currently? I tried presubmitting and it seems maybe some people were depending on that quirk. You can leave a TODO to clean it up later, just don't want it to block this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously I can, but that second await this._stabilize() was recently added in #20400 which makes that there cannot be many people, if any at all, relying on it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible the failures I saw were not related to this line. I'll do some more digging and let you know

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirmed that removing that call to this._stabilize() was indeed the culprit. It'll need to be investigated, but shouldn't block this PR. Can you add it back with a comment to investigate why removing it breaks some tests in g3?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do


// 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();
}
}
39 changes: 39 additions & 0 deletions src/cdk/testing/tests/cross-environment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,27 +320,66 @@ 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();
await clickTest.click(10, 10);
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();
await clickTest.click('center');
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();
await clickTest.rightClick!(50, 50);
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();
Expand Down
1 change: 1 addition & 0 deletions src/cdk/testing/tests/harnesses/main-component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
1 change: 1 addition & 0 deletions src/cdk/testing/tests/test-main-component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
</div>
<div class="click-test-result">{{clickResult.x}}-{{clickResult.y}}</div>
<div class="contextmenu-test-result">{{rightClickResult.x}}-{{rightClickResult.y}}-{{rightClickResult.button}}</div>
<div class="click-modifiers-test-result">{{modifiers}}</div>
<h1 style="height: 100px; width: 200px;">Main Component</h1>
<div id="username">Hello {{username}} from Angular 2!</div>
<div class="counters">
Expand Down
7 changes: 7 additions & 0 deletions src/cdk/testing/tests/test-main-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class TestMainComponent implements OnDestroy {
testMethods: string[];
isHovering = false;
specialKey = '';
modifiers: string;
singleSelect: string;
singleSelectChangeEventCount = 0;
multiSelect: string[] = [];
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 4 additions & 4 deletions tools/public_api_guard/cdk/testing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ export declare function stopHandlingAutoChangeDetectionStatus(): void;
export interface TestElement {
blur(): Promise<void>;
clear(): Promise<void>;
click(): Promise<void>;
click(location: 'center'): Promise<void>;
click(relativeX: number, relativeY: number): Promise<void>;
click(modifiers?: ModifierKeys): Promise<void>;
click(location: 'center', modifiers?: ModifierKeys): Promise<void>;
click(relativeX: number, relativeY: number, modifiers?: ModifierKeys): Promise<void>;
dispatchEvent?(name: string, data?: Record<string, EventData>): Promise<void>;
focus(): Promise<void>;
getAttribute(name: string): Promise<string | null>;
Expand All @@ -165,7 +165,7 @@ export interface TestElement {
isFocused(): Promise<boolean>;
matchesSelector(selector: string): Promise<boolean>;
mouseAway(): Promise<void>;
rightClick?(relativeX: number, relativeY: number): Promise<void>;
rightClick?(relativeX: number, relativeY: number, modifiers?: ModifierKeys): Promise<void>;
selectOptions?(...optionIndexes: number[]): Promise<void>;
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
Expand Down
12 changes: 10 additions & 2 deletions tools/public_api_guard/cdk/testing/protractor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ export declare class ProtractorElement implements TestElement {
constructor(element: ElementFinder);
blur(): Promise<void>;
clear(): Promise<void>;
click(...args: [] | ['center'] | [number, number]): Promise<void>;
click(...args: [ModifierKeys?] | ['center', ModifierKeys?] | [
number,
number,
ModifierKeys?
]): Promise<void>;
dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void>;
focus(): Promise<void>;
getAttribute(name: string): Promise<string | null>;
Expand All @@ -15,7 +19,11 @@ export declare class ProtractorElement implements TestElement {
isFocused(): Promise<boolean>;
matchesSelector(selector: string): Promise<boolean>;
mouseAway(): Promise<void>;
rightClick(...args: [] | ['center'] | [number, number]): Promise<void>;
rightClick(...args: [ModifierKeys?] | ['center', ModifierKeys?] | [
number,
number,
ModifierKeys?
]): Promise<void>;
selectOptions(...optionIndexes: number[]): Promise<void>;
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
Expand Down
12 changes: 10 additions & 2 deletions tools/public_api_guard/cdk/testing/testbed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export declare class UnitTestElement implements TestElement {
constructor(element: Element, _stabilize: () => Promise<void>);
blur(): Promise<void>;
clear(): Promise<void>;
click(...args: [] | ['center'] | [number, number]): Promise<void>;
click(...args: [ModifierKeys?] | ['center', ModifierKeys?] | [
number,
number,
ModifierKeys?
]): Promise<void>;
dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void>;
focus(): Promise<void>;
getAttribute(name: string): Promise<string | null>;
Expand All @@ -33,7 +37,11 @@ export declare class UnitTestElement implements TestElement {
isFocused(): Promise<boolean>;
matchesSelector(selector: string): Promise<boolean>;
mouseAway(): Promise<void>;
rightClick(...args: [] | ['center'] | [number, number]): Promise<void>;
rightClick(...args: [ModifierKeys?] | ['center', ModifierKeys?] | [
number,
number,
ModifierKeys?
]): Promise<void>;
selectOptions(...optionIndexes: number[]): Promise<void>;
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
Expand Down