From 1d15ea77d817e65b8d318f801051441845cd7fe1 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 13 Jun 2022 09:22:47 +0200 Subject: [PATCH] fix(cdk/testing): simulate offsetX and offsetY in mouse events 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. --- .../testbed/fake-events/dispatch-events.ts | 14 ++++- .../testbed/fake-events/event-objects.ts | 25 +++++++- src/cdk/testing/testbed/unit-test-element.ts | 61 ++++++++++++++++--- .../mdc-menu/menu.spec.ts | 2 +- .../mdc-tabs/tab-header.spec.ts | 5 +- src/material/menu/menu.spec.ts | 2 +- src/material/slider/slider.spec.ts | 2 +- src/material/tabs/tab-header.spec.ts | 5 +- 8 files changed, 96 insertions(+), 20 deletions(-) diff --git a/src/cdk/testing/testbed/fake-events/dispatch-events.ts b/src/cdk/testing/testbed/fake-events/dispatch-events.ts index 555a2854fe91..b66300187159 100644 --- a/src/cdk/testing/testbed/fake-events/dispatch-events.ts +++ b/src/cdk/testing/testbed/fake-events/dispatch-events.ts @@ -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), + ); } /** @@ -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; } /** diff --git a/src/cdk/testing/testbed/fake-events/event-objects.ts b/src/cdk/testing/testbed/fake-events/event-objects.ts index d68892693ce5..4bbc10e03f48 100644 --- a/src/cdk/testing/testbed/fake-events/event-objects.ts +++ b/src/cdk/testing/testbed/fake-events/event-objects.ts @@ -19,6 +19,8 @@ export function createMouseEvent( type: string, clientX = 0, clientY = 0, + offsetX = 1, + offsetY = 1, button = 0, modifiers: ModifierKeys = {}, ) { @@ -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; } @@ -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. @@ -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; } /** diff --git a/src/cdk/testing/testbed/unit-test-element.ts b/src/cdk/testing/testbed/unit-test-element.ts index 915ce72980cb..fedd66367723 100644 --- a/src/cdk/testing/testbed/unit-test-element.ts +++ b/src/cdk/testing/testbed/unit-test-element.ts @@ -283,6 +283,8 @@ 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. @@ -290,7 +292,10 @@ export class UnitTestElement implements TestElement { // 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, + }); } } @@ -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') { @@ -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- diff --git a/src/material-experimental/mdc-menu/menu.spec.ts b/src/material-experimental/mdc-menu/menu.spec.ts index 43ba01ee11b8..f2a2809f4836 100644 --- a/src/material-experimental/mdc-menu/menu.spec.ts +++ b/src/material-experimental/mdc-menu/menu.spec.ts @@ -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(); diff --git a/src/material-experimental/mdc-tabs/tab-header.spec.ts b/src/material-experimental/mdc-tabs/tab-header.spec.ts index ea22e607d19a..ac5c2639200b 100644 --- a/src/material-experimental/mdc-tabs/tab-header.spec.ts +++ b/src/material-experimental/mdc-tabs/tab-header.spec.ts @@ -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); diff --git a/src/material/menu/menu.spec.ts b/src/material/menu/menu.spec.ts index 8dc06ec502f4..a4d947d6f0ea 100644 --- a/src/material/menu/menu.spec.ts +++ b/src/material/menu/menu.spec.ts @@ -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(); diff --git a/src/material/slider/slider.spec.ts b/src/material/slider/slider.spec.ts index 04e92cd4fbb8..a6fc53808d2b 100644 --- a/src/material/slider/slider.spec.ts +++ b/src/material/slider/slider.spec.ts @@ -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)); } /** diff --git a/src/material/tabs/tab-header.spec.ts b/src/material/tabs/tab-header.spec.ts index a60b8b64a43c..9c498482a5c0 100644 --- a/src/material/tabs/tab-header.spec.ts +++ b/src/material/tabs/tab-header.spec.ts @@ -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);