Skip to content

Commit

Permalink
fix(cdk/testing): simulate offsetX and offsetY in mouse events
Browse files Browse the repository at this point in the history
We already supported dispatching mouse events at specific element coordinates, but we were using them only to set `clientX` and `clientY`. These changes also set `offsetX` and `offsetY` since they're the native representation of element-relative coordinates.
  • Loading branch information
crisbeto committed Jun 13, 2022
1 parent aadcb96 commit 1d15ea7
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 20 deletions.
14 changes: 12 additions & 2 deletions src/cdk/testing/testbed/fake-events/dispatch-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,15 @@ export function dispatchMouseEvent(
type: string,
clientX = 0,
clientY = 0,
offsetX?: number,
offsetY?: number,
button?: number,
modifiers?: ModifierKeys,
): MouseEvent {
return dispatchEvent(node, createMouseEvent(type, clientX, clientY, button, modifiers));
return dispatchEvent(
node,
createMouseEvent(type, clientX, clientY, offsetX, offsetY, button, modifiers),
);
}

/**
Expand All @@ -71,9 +76,14 @@ export function dispatchPointerEvent(
type: string,
clientX = 0,
clientY = 0,
offsetX?: number,
offsetY?: number,
options?: PointerEventInit,
): PointerEvent {
return dispatchEvent(node, createPointerEvent(type, clientX, clientY, options)) as PointerEvent;
return dispatchEvent(
node,
createPointerEvent(type, clientX, clientY, offsetX, offsetY, options),
) as PointerEvent;
}

/**
Expand Down
25 changes: 22 additions & 3 deletions src/cdk/testing/testbed/fake-events/event-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export function createMouseEvent(
type: string,
clientX = 0,
clientY = 0,
offsetX = 1,
offsetY = 1,
button = 0,
modifiers: ModifierKeys = {},
) {
Expand Down Expand Up @@ -50,8 +52,13 @@ export function createMouseEvent(

// The `MouseEvent` constructor doesn't allow us to pass these properties into the constructor.
// Override them to `1`, because they're used for fake screen reader event detection.
defineReadonlyEventProperty(event, 'offsetX', 1);
defineReadonlyEventProperty(event, 'offsetY', 1);
if (offsetX != null) {
defineReadonlyEventProperty(event, 'offsetX', offsetX);
}

if (offsetY != null) {
defineReadonlyEventProperty(event, 'offsetY', offsetY);
}

return event;
}
Expand All @@ -70,9 +77,11 @@ export function createPointerEvent(
type: string,
clientX = 0,
clientY = 0,
offsetX?: number,
offsetY?: number,
options: PointerEventInit = {isPrimary: true},
) {
return new PointerEvent(type, {
const event = new PointerEvent(type, {
bubbles: true,
cancelable: true,
composed: true, // Required for shadow DOM events.
Expand All @@ -81,6 +90,16 @@ export function createPointerEvent(
clientY,
...options,
});

if (offsetX != null) {
defineReadonlyEventProperty(event, 'offsetX', offsetX);
}

if (offsetY != null) {
defineReadonlyEventProperty(event, 'offsetY', offsetY);
}

return event;
}

/**
Expand Down
61 changes: 51 additions & 10 deletions src/cdk/testing/testbed/unit-test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,19 @@ export class UnitTestElement implements TestElement {
name: string,
clientX?: number,
clientY?: number,
offsetX?: number,
offsetY?: 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, {isPrimary: true, button});
dispatchPointerEvent(this.element, name, clientX, clientY, offsetX, offsetY, {
isPrimary: true,
button,
});
}
}

Expand All @@ -305,6 +310,8 @@ export class UnitTestElement implements TestElement {
) {
let clientX: number | undefined = undefined;
let clientY: number | undefined = undefined;
let offsetX: number | undefined = undefined;
let offsetY: number | undefined = undefined;
let modifiers: ModifierKeys = {};

if (args.length && typeof args[args.length - 1] === 'object') {
Expand All @@ -313,23 +320,57 @@ export class UnitTestElement implements TestElement {

if (args.length) {
const {left, top, width, height} = await this.getDimensions();
const relativeX = args[0] === 'center' ? width / 2 : (args[0] as number);
const relativeY = args[0] === 'center' ? height / 2 : (args[1] as number);
offsetX = args[0] === 'center' ? width / 2 : (args[0] as number);
offsetY = 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.
clientX = Math.round(left + relativeX);
clientY = Math.round(top + relativeY);
clientX = Math.round(left + offsetX);
clientY = Math.round(top + offsetY);
}

this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY, button);
dispatchMouseEvent(this.element, 'mousedown', clientX, clientY, button, modifiers);
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY, button);
dispatchMouseEvent(this.element, 'mouseup', clientX, clientY, button, modifiers);
this._dispatchPointerEventIfSupported(
'pointerdown',
clientX,
clientY,
offsetX,
offsetY,
button,
);
dispatchMouseEvent(
this.element,
'mousedown',
clientX,
clientY,
offsetX,
offsetY,
button,
modifiers,
);
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY, offsetX, offsetY, button);
dispatchMouseEvent(
this.element,
'mouseup',
clientX,
clientY,
offsetX,
offsetY,
button,
modifiers,
);

// If a primary event name is specified, emit it after the mouse event sequence.
if (primaryEventName !== null) {
dispatchMouseEvent(this.element, primaryEventName, clientX, clientY, button, modifiers);
dispatchMouseEvent(
this.element,
primaryEventName,
clientX,
clientY,
offsetX,
offsetY,
button,
modifiers,
);
}

// This call to _stabilize should not be needed since the callers will already do that them-
Expand Down
2 changes: 1 addition & 1 deletion src/material-experimental/mdc-menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ describe('MDC-based MatMenu', () => {
focusMonitor.monitor(triggerEl, false);

// Trigger a fake right click.
dispatchEvent(triggerEl, createMouseEvent('mousedown', 50, 100, 2));
dispatchEvent(triggerEl, createMouseEvent('mousedown', 50, 100, undefined, undefined, 2));

// A click without a left button mousedown before it is considered a keyboard open.
triggerEl.click();
Expand Down
5 changes: 4 additions & 1 deletion src/material-experimental/mdc-tabs/tab-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,10 @@ describe('MDC-based MatTabHeader', () => {
it('should not scroll when pressing the right mouse button', fakeAsync(() => {
expect(header.scrollDistance).withContext('Expected to start off not scrolled.').toBe(0);

dispatchEvent(nextButton, createMouseEvent('mousedown', undefined, undefined, 2));
dispatchEvent(
nextButton,
createMouseEvent('mousedown', undefined, undefined, undefined, undefined, 2),
);
fixture.detectChanges();
tick(3000);

Expand Down
2 changes: 1 addition & 1 deletion src/material/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ describe('MatMenu', () => {
focusMonitor.monitor(triggerEl, false);

// Trigger a fake right click.
dispatchEvent(triggerEl, createMouseEvent('mousedown', 50, 100, 2));
dispatchEvent(triggerEl, createMouseEvent('mousedown', 50, 100, undefined, undefined, 2));

// A click without a left button mousedown before it is considered a keyboard open.
triggerEl.click();
Expand Down
2 changes: 1 addition & 1 deletion src/material/slider/slider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1778,7 +1778,7 @@ function dispatchMousedownEventSequence(
const y = dimensions.top + dimensions.height * percentage;

dispatchMouseenterEvent(sliderElement);
dispatchEvent(sliderElement, createMouseEvent('mousedown', x, y, button));
dispatchEvent(sliderElement, createMouseEvent('mousedown', x, y, undefined, undefined, button));
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/material/tabs/tab-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,10 @@ describe('MatTabHeader', () => {
it('should not scroll when pressing the right mouse button', fakeAsync(() => {
expect(header.scrollDistance).withContext('Expected to start off not scrolled.').toBe(0);

dispatchEvent(nextButton, createMouseEvent('mousedown', undefined, undefined, 2));
dispatchEvent(
nextButton,
createMouseEvent('mousedown', undefined, undefined, undefined, undefined, 2),
);
fixture.detectChanges();
tick(3000);

Expand Down

0 comments on commit 1d15ea7

Please sign in to comment.