From fca57645646e8733c3cef62c3f4dc7d2e5da658b Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Thu, 13 Jun 2024 11:46:58 -0700 Subject: [PATCH] Revert "refactor(core): Use `ActionResolver` in `Dispatcher`. (#56369)" (#56440) This reverts commit 4ebd2853fa7a9b47c71ff19b5a6a819502905508. PR Close #56440 --- .../primitives/event-dispatch/index.api.md | 2 +- .../event-dispatch/src/eventcontract.ts | 2 +- .../event-dispatch/test/dispatcher_test.ts | 872 ++------------ .../event-dispatch/test/eventcontract_test.ts | 1016 +++++++++++++++-- packages/core/src/event_delegation_utils.ts | 5 +- packages/core/src/hydration/event_replay.ts | 1 - 6 files changed, 1016 insertions(+), 882 deletions(-) diff --git a/goldens/public-api/core/primitives/event-dispatch/index.api.md b/goldens/public-api/core/primitives/event-dispatch/index.api.md index e9fd5f3aad6..3d69b9de0b6 100644 --- a/goldens/public-api/core/primitives/event-dispatch/index.api.md +++ b/goldens/public-api/core/primitives/event-dispatch/index.api.md @@ -25,7 +25,7 @@ export interface EarlyJsactionDataContainer { // @public export class EventContract implements UnrenamedEventContract { - constructor(containerManager: EventContractContainerManager, useActionResolver: false); + constructor(containerManager: EventContractContainerManager, useActionResolver?: boolean); // (undocumented) static A11Y_CLICK_SUPPORT: boolean; addA11yClickSupport(): void; diff --git a/packages/core/primitives/event-dispatch/src/eventcontract.ts b/packages/core/primitives/event-dispatch/src/eventcontract.ts index 90acc044a24..53c969f1069 100644 --- a/packages/core/primitives/event-dispatch/src/eventcontract.ts +++ b/packages/core/primitives/event-dispatch/src/eventcontract.ts @@ -127,7 +127,7 @@ export class EventContract implements UnrenamedEventContract { constructor( containerManager: EventContractContainerManager, - private readonly useActionResolver: false, + private readonly useActionResolver = true, ) { this.containerManager = containerManager; if (this.useActionResolver) { diff --git a/packages/core/primitives/event-dispatch/test/dispatcher_test.ts b/packages/core/primitives/event-dispatch/test/dispatcher_test.ts index d08dc5bea4b..d9380abff7c 100644 --- a/packages/core/primitives/event-dispatch/test/dispatcher_test.ts +++ b/packages/core/primitives/event-dispatch/test/dispatcher_test.ts @@ -6,101 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ -import * as cache from '../src/cache'; import {ActionInfo, createEventInfo, EventInfoWrapper} from '../src/event_info'; import {Dispatcher, registerDispatcher, Replayer} from '../src/dispatcher'; -import {EventContract} from '../src/eventcontract'; +import {addDeferredA11yClickSupport, EventContract} from '../src/eventcontract'; import {EventContractContainer} from '../src/event_contract_container'; import {safeElement, testonlyHtml} from './html'; -import {ActionResolver} from '../src/action_resolver'; -import { - populateClickOnlyAction, - preventDefaultForA11yClick, - updateEventInfoForA11yClick, -} from '../src/a11y_click'; -import {OWNER} from '../src/property'; const domContent = ` -
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
- - - -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
`; function getRequiredElementById(id: string) { @@ -160,10 +53,7 @@ function createEventContract({ eventTypes: Array; exportAddA11yClickSupport?: boolean; }): EventContract { - const eventContract = new EventContract( - new EventContractContainer(container), - /* useActionResolver= */ false, - ); + const eventContract = new EventContract(new EventContractContainer(container)); if (exportAddA11yClickSupport) { eventContract.exportAddA11yClickSupport(); } @@ -307,38 +197,6 @@ function createTestEventInfoWrapper({ ); } -function createDispatchDelegateSpy() { - return jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate'); -} - -function createDispatcher({ - dispatchDelegate, - eventContract, - eventReplayer, - a11yClickSupport = false, - syntheticMouseEventSupport = false, -}: { - dispatchDelegate: (eventInfoWrapper: EventInfoWrapper) => void; - eventContract?: EventContract; - eventReplayer?: Replayer; - a11yClickSupport?: boolean; - syntheticMouseEventSupport?: boolean; -}) { - const actionResolver = new ActionResolver({syntheticMouseEventSupport}); - if (a11yClickSupport) { - actionResolver.addA11yClickSupport( - updateEventInfoForA11yClick, - preventDefaultForA11yClick, - populateClickOnlyAction, - ); - } - const dispatcher = new Dispatcher(dispatchDelegate, {actionResolver, eventReplayer}); - if (eventContract) { - registerDispatcher(eventContract, dispatcher); - } - return dispatcher; -} - describe('Dispatcher', () => { beforeEach(() => { safeElement.setInnerHtml(document.body, testonlyHtml(domContent)); @@ -347,319 +205,21 @@ describe('Dispatcher', () => { spyOn(Date, 'now').and.returnValue(0); }); - it('dispatches event', () => { - const container = getRequiredElementById('click-container'); - const actionElement = getRequiredElementById('click-action-element'); - const targetElement = getRequiredElementById('click-target-element'); + it('dispatches to dispatchDelegate', () => { + const dispatchDelegate = + jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate'); + const dispatcher = new Dispatcher(dispatchDelegate); + const eventInfoWrapper = createTestEventInfoWrapper(); - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); + dispatcher.dispatch(eventInfoWrapper.eventInfo); - const clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + expect(dispatchDelegate).toHaveBeenCalledWith(eventInfoWrapper); }); - it('dispatches event when targetElement is actionElement', () => { - const container = getRequiredElementById('self-click-container'); - const targetElement = getRequiredElementById('self-click-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - const clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(targetElement); - }); - - it('dispatch event to child and ignore parent', () => { - const container = getRequiredElementById('parent-and-child-container'); - const actionElement = getRequiredElementById('parent-and-child-action-element'); - const targetElement = getRequiredElementById('parent-and-child-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - const clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('childHandleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('dispatch event through owner', () => { - const container = getRequiredElementById('owner-click-container'); - const actionElement = getRequiredElementById('owner-click-action-element'); - const targetElement = getRequiredElementById('owner-click-target-element'); - targetElement[OWNER] = actionElement; - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - const clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('ownerHandleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('dispatches modified click event', () => { - const container = getRequiredElementById('clickmod-container'); - const actionElement = getRequiredElementById('clickmod-action-element'); - const targetElement = getRequiredElementById('clickmod-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - const clickEvent = dispatchMouseEvent(targetElement, {shiftKey: true}); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('clickmod'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClickMod'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('caches jsaction attribute', () => { - const container = getRequiredElementById('click-container'); - const actionElement = getRequiredElementById('click-action-element'); - const targetElement = getRequiredElementById('click-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - let clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - let eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - - actionElement.setAttribute('jsaction', 'renamedHandleClick'); - dispatchDelegate.calls.reset(); - - clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('re-parses jsaction attribute if the action cache is cleared', () => { - const container = getRequiredElementById('click-container'); - const actionElement = getRequiredElementById('click-action-element'); - const targetElement = getRequiredElementById('click-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - let clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - let eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - - actionElement.setAttribute('jsaction', 'renamedHandleClick'); - // Clear attribute cache. - cache.clear(actionElement); - dispatchDelegate.calls.reset(); - - clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('renamedHandleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('handles trailing semicolon in jsaction attribute', () => { - const container = getRequiredElementById('trailing-semicolon-container'); - const actionElement = getRequiredElementById('trailing-semicolon-action-element'); - const targetElement = getRequiredElementById('trailing-semicolon-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - const clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('handles jsaction attributes without action names, first action', () => { - const container = getRequiredElementById('no-action-name-container'); - const actionElement = getRequiredElementById('no-action-name-action-element'); - const targetElement = getRequiredElementById('no-action-name-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click', 'keydown', 'keyup'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - const keydownEvent = dispatchKeyboardEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('keydown'); - expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe(''); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('handles jsaction attributes without action names, last action', () => { - const container = getRequiredElementById('no-action-name-container'); - const actionElement = getRequiredElementById('no-action-name-action-element'); - const targetElement = getRequiredElementById('no-action-name-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click', 'keydown', 'keyup'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - const keyupEvent = dispatchKeyboardEvent(targetElement, {type: 'keyup'}); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('keyup'); - expect(eventInfoWrapper.getEvent()).toBe(keyupEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe(''); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('does not handle jsaction attributes without event type or action name', () => { - const container = getRequiredElementById('no-action-name-container'); - const targetElement = getRequiredElementById('no-action-name-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click', 'keydown', 'keyup'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - const clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); - }); - - it('dispatches event from shadow dom', () => { - const container = getRequiredElementById('shadow-dom-container'); - const actionElement = getRequiredElementById('shadow-dom-action-element'); - - // Not supported in ie11. - if (!actionElement.attachShadow) { - return; - } - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); - - const shadow = actionElement.attachShadow({mode: 'open'}); - const shadowChild = document.createElement('div'); - shadow.appendChild(shadowChild); - - shadowChild.click(); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - // Target element is set to the host from the event. - expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('replays to dispatchDelegate', () => { - const dispatchDelegate = createDispatchDelegateSpy(); - const dispatcher = createDispatcher({dispatchDelegate}); + it('replays to dispatchDelegate', async () => { + const dispatchDelegate = + jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate'); + const dispatcher = new Dispatcher(dispatchDelegate); const eventInfoWrappers = [ createTestEventInfoWrapper({isReplay: true}), createTestEventInfoWrapper({isReplay: true}), @@ -670,6 +230,8 @@ describe('Dispatcher', () => { dispatcher.dispatch(eventInfoWrapper.eventInfo); } + await Promise.resolve(); + expect(dispatchDelegate).toHaveBeenCalledTimes(3); for (let i = 0; i < eventInfoWrappers.length; i++) { expect(dispatchDelegate.calls.argsFor(i)).toEqual([eventInfoWrappers[i]]); @@ -677,9 +239,10 @@ describe('Dispatcher', () => { }); it('replays to event replayer', async () => { - const dispatchDelegate = createDispatchDelegateSpy(); + const dispatchDelegate = + jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate'); const eventReplayer = jasmine.createSpy('eventReplayer'); - const dispatcher = createDispatcher({dispatchDelegate, eventReplayer}); + const dispatcher = new Dispatcher(dispatchDelegate, {eventReplayer}); const eventInfoWrappers = [ createTestEventInfoWrapper({isReplay: true}), createTestEventInfoWrapper({isReplay: true}), @@ -705,13 +268,14 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); + const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); + const dispatcher = new Dispatcher(dispatch); + registerDispatcher(eventContract, dispatcher); const clickEvent = dispatchMouseEvent(targetElement); - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; + expect(dispatch).toHaveBeenCalledTimes(1); + const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); @@ -730,14 +294,14 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatchDelegate = - jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate'); - createDispatcher({dispatchDelegate, eventContract}); + const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); + const dispatcher = new Dispatcher(dispatch); + registerDispatcher(eventContract, dispatcher); const clickEvent = dispatchMouseEvent(targetElement, {shiftKey: true}); - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; + expect(dispatch).toHaveBeenCalledTimes(1); + const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; expect(eventInfoWrapper.getEventType()).toBe('clickmod'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); @@ -756,13 +320,14 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract}); + const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); + const dispatcher = new Dispatcher(dispatch); + registerDispatcher(eventContract, dispatcher); const clickEvent = dispatchMouseEvent(targetElement, {shiftKey: true}); - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; + expect(dispatch).toHaveBeenCalledTimes(1); + const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; expect(eventInfoWrapper.getEventType()).toBe('clickmod'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); @@ -773,103 +338,11 @@ describe('Dispatcher', () => { }); describe('a11y click', () => { - beforeEach(() => { + beforeAll(() => { EventContract.A11Y_CLICK_SUPPORT = true; }); - afterEach(() => { - EventContract.A11Y_CLICK_SUPPORT = true; - }); - - it('dispatches keydown as click event', () => { - const container = getRequiredElementById('a11y-click-container'); - const actionElement = getRequiredElementById('a11y-click-action-element'); - const targetElement = getRequiredElementById('a11y-click-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, a11yClickSupport: true}); - - const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'}); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('dispatches keydown event', () => { - const container = getRequiredElementById('keydown-container'); - const actionElement = getRequiredElementById('keydown-action-element'); - const targetElement = getRequiredElementById('keydown-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['keydown'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, a11yClickSupport: true}); - - const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'a'}); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('keydown'); - expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleKeydown'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('dispatches clickonly event', () => { - const container = getRequiredElementById('a11y-clickonly-container'); - const actionElement = getRequiredElementById('a11y-clickonly-action-element'); - const targetElement = getRequiredElementById('a11y-clickonly-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, a11yClickSupport: true}); - - const clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('clickonly'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClickOnly'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('dispatches click event to click handler rather than clickonly', () => { - const container = getRequiredElementById('a11y-click-clickonly-container'); - const actionElement = getRequiredElementById('a11y-click-clickonly-action-element'); - const targetElement = getRequiredElementById('a11y-click-clickonly-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['click'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, a11yClickSupport: true}); - - const clickEvent = dispatchMouseEvent(targetElement); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('click'); - expect(eventInfoWrapper.getEvent()).toBe(clickEvent); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + afterAll(() => { + EventContract.A11Y_CLICK_SUPPORT = false; }); it('prevents default for enter key on anchor child', () => { @@ -881,13 +354,14 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, a11yClickSupport: true}); + const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); + const dispatcher = new Dispatcher(dispatch); + registerDispatcher(eventContract, dispatcher); const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'}); - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; + expect(dispatch).toHaveBeenCalledTimes(1); + const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); @@ -906,13 +380,14 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, a11yClickSupport: true}); + const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); + const dispatcher = new Dispatcher(dispatch); + registerDispatcher(eventContract, dispatcher); const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'}); - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; + expect(dispatch).toHaveBeenCalledTimes(1); + const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); @@ -923,252 +398,33 @@ describe('Dispatcher', () => { }); }); - describe('non-bubbling mouse events', () => { - beforeEach(() => { - EventContract.MOUSE_SPECIAL_SUPPORT = true; - }); - afterEach(() => { - EventContract.MOUSE_SPECIAL_SUPPORT = false; - }); - - it('dispatches matching mouseover as mouseenter event', () => { - const container = getRequiredElementById('mouseenter-container'); - const actionElement = getRequiredElementById('mouseenter-action-element'); - const targetElement = getRequiredElementById('mouseenter-target-element'); + describe('a11y click support deferred', () => { + it('prevents default for enter key on anchor child', () => { + const container = getRequiredElementById('a11y-anchor-click-container'); + const actionElement = getRequiredElementById('a11y-anchor-click-action-element'); + const targetElement = getRequiredElementById('a11y-anchor-click-target-element'); const eventContract = createEventContract({ container, - eventTypes: ['mouseenter'], + exportAddA11yClickSupport: true, + eventTypes: ['click'], }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, syntheticMouseEventSupport: true}); + addDeferredA11yClickSupport(eventContract); + const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); + const dispatcher = new Dispatcher(dispatch); + registerDispatcher(eventContract, dispatcher); - dispatchMouseEvent(targetElement, { - type: 'mouseover', - // Indicates that the mouse exited the container and entered the - // target element. - relatedTarget: container, - }); + const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'}); - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('mouseenter'); - const syntheticMouseEvent = eventInfoWrapper.getEvent(); - expect(syntheticMouseEvent.type).toBe('mouseenter'); - expect(syntheticMouseEvent.target).toBe(actionElement); - expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleMouseEnter'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('does not dispatch non-matching mouseover event as mouseenter', () => { - const container = getRequiredElementById('mouseenter-container'); - const actionElement = getRequiredElementById('mouseenter-action-element'); - const targetElement = getRequiredElementById('mouseenter-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['mouseenter'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, syntheticMouseEventSupport: true}); - - dispatchMouseEvent(targetElement, { - type: 'mouseover', - // Indicates that the mouse exited the action element and entered the - // target element. - relatedTarget: actionElement, - }); - - // For failed `mouseenter` events, a global event is still dispatched without an action. - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('mouseenter'); - const mouseEvent = eventInfoWrapper.getEvent(); - expect(mouseEvent.type).toBe('mouseover'); - expect(mouseEvent.target).toBe(targetElement); + expect(dispatch).toHaveBeenCalledTimes(1); + const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); - }); - - it('dispatches matching mouseout as mouseleave event', () => { - const container = getRequiredElementById('mouseleave-container'); - const actionElement = getRequiredElementById('mouseleave-action-element'); - const targetElement = getRequiredElementById('mouseleave-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['mouseleave'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, syntheticMouseEventSupport: true}); - - dispatchMouseEvent(targetElement, { - type: 'mouseout', - // Indicates that the mouse entered the container and exited the - // target element. - relatedTarget: container, - }); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('mouseleave'); - const syntheticMouseEvent = eventInfoWrapper.getEvent(); - expect(syntheticMouseEvent.type).toBe('mouseleave'); - expect(syntheticMouseEvent.target).toBe(actionElement); - expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleMouseLeave'); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - it('does not dispatch non-matching mouseout event as mouseleave', () => { - const container = getRequiredElementById('mouseleave-container'); - const actionElement = getRequiredElementById('mouseleave-action-element'); - const targetElement = getRequiredElementById('mouseleave-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['mouseleave'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, syntheticMouseEventSupport: true}); - - dispatchMouseEvent(targetElement, { - type: 'mouseout', - // Indicates that the mouse entered the action element and exited the - // target element. - relatedTarget: actionElement, - }); - - // For failed `mouseleave` events, a global event is still dispatched without an action. - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('mouseleave'); - const mouseEvent = eventInfoWrapper.getEvent(); - expect(mouseEvent.type).toBe('mouseout'); - expect(mouseEvent.target).toBe(targetElement); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); - }); - - it('dispatches matching pointerover as pointerenter event', () => { - const container = getRequiredElementById('pointerenter-container'); - const actionElement = getRequiredElementById('pointerenter-action-element'); - const targetElement = getRequiredElementById('pointerenter-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['pointerenter'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, syntheticMouseEventSupport: true}); - - dispatchMouseEvent(targetElement, { - type: 'pointerover', - // Indicates that the pointer exited the container and entered the - // target element. - relatedTarget: container, - }); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('pointerenter'); - const syntheticMouseEvent = eventInfoWrapper.getEvent(); - expect(syntheticMouseEvent.type).toBe('pointerenter'); - expect(syntheticMouseEvent.target).toBe(actionElement); - expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handlePointerEnter'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('does not dispatch non-matching pointerover event as pointerenter', () => { - const container = getRequiredElementById('pointerenter-container'); - const actionElement = getRequiredElementById('pointerenter-action-element'); - const targetElement = getRequiredElementById('pointerenter-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['pointerenter'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, syntheticMouseEventSupport: true}); - - dispatchMouseEvent(targetElement, { - type: 'pointerover', - // Indicates that the pointer exited the action element and entered the - // target element. - relatedTarget: actionElement, - }); - - // For failed `pointerenter` events, a global event is still dispatched without an action. - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('pointerenter'); - const mouseEvent = eventInfoWrapper.getEvent(); - expect(mouseEvent.type).toBe('pointerover'); - expect(mouseEvent.target).toBe(targetElement); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); - }); - - it('dispatches matching pointerout as pointerleave event', () => { - const container = getRequiredElementById('pointerleave-container'); - const actionElement = getRequiredElementById('pointerleave-action-element'); - const targetElement = getRequiredElementById('pointerleave-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['pointerleave'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, syntheticMouseEventSupport: true}); - - dispatchMouseEvent(targetElement, { - type: 'pointerout', - // Indicates that the pointer entered the container and exited the - // target element. - relatedTarget: container, - }); - - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('pointerleave'); - const syntheticMouseEvent = eventInfoWrapper.getEvent(); - expect(syntheticMouseEvent.type).toBe('pointerleave'); - expect(syntheticMouseEvent.target).toBe(actionElement); - expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handlePointerLeave'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); - }); - - it('does not dispatch non-matching pointerout event as pointerleave', () => { - const container = getRequiredElementById('pointerleave-container'); - const actionElement = getRequiredElementById('pointerleave-action-element'); - const targetElement = getRequiredElementById('pointerleave-target-element'); - - const eventContract = createEventContract({ - container, - eventTypes: ['pointerleave'], - }); - const dispatchDelegate = createDispatchDelegateSpy(); - createDispatcher({dispatchDelegate, eventContract, syntheticMouseEventSupport: true}); - - dispatchMouseEvent(targetElement, { - type: 'pointerout', - // Indicates that the pointer entered the action element and exited the - // target element. - relatedTarget: actionElement, - }); - - // For failed `pointerleave` events, a global event is still dispatched without an action. - expect(dispatchDelegate).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatchDelegate.calls.mostRecent().args[0]; - expect(eventInfoWrapper.getEventType()).toBe('pointerleave'); - const mouseEvent = eventInfoWrapper.getEvent(); - expect(mouseEvent.type).toBe('pointerout'); - expect(mouseEvent.target).toBe(targetElement); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(keydownEvent.preventDefault).toHaveBeenCalled(); }); }); }); diff --git a/packages/core/primitives/event-dispatch/test/eventcontract_test.ts b/packages/core/primitives/event-dispatch/test/eventcontract_test.ts index 26f75e740e5..d3e050a8da5 100644 --- a/packages/core/primitives/event-dispatch/test/eventcontract_test.ts +++ b/packages/core/primitives/event-dispatch/test/eventcontract_test.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import * as cache from '../src/cache'; import { EarlyEventContract, EarlyJsactionData, @@ -18,7 +19,8 @@ import { import {EventContractMultiContainer} from '../src/event_contract_multi_container'; import {EventInfoWrapper} from '../src/event_info'; import {EventType} from '../src/event_type'; -import {Dispatcher, EventContract} from '../src/eventcontract'; +import {addDeferredA11yClickSupport, Dispatcher, EventContract} from '../src/eventcontract'; +import {OWNER} from '../src/property'; import {Restriction} from '../src/restriction'; import {safeElement, testonlyHtml} from './html'; @@ -39,18 +41,98 @@ const domContent = `
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ + + +
+
+
+ +
+ +
+
+
+ +
+
@@ -63,12 +145,30 @@ const domContent = `
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
@@ -93,17 +193,19 @@ function createEventContractMultiContainer( function createEventContract({ eventContractContainerManager, + exportAddA11yClickSupport = false, eventTypes, dispatcher, }: { eventContractContainerManager: EventContractContainerManager; + exportAddA11yClickSupport?: boolean; eventTypes: Array; dispatcher?: jasmine.Spy; }): EventContract { - const eventContract = new EventContract( - eventContractContainerManager, - /* useActionResolver= */ false, - ); + const eventContract = new EventContract(eventContractContainerManager); + if (exportAddA11yClickSupport) { + eventContract.exportAddA11yClickSupport(); + } for (const eventType of eventTypes) { if (typeof eventType === 'string') { eventContract.addEvent(eventType); @@ -118,10 +220,6 @@ function createEventContract({ return eventContract; } -function createDispatcherSpy() { - return jasmine.createSpy('dispatcher'); -} - function getLastDispatchedEventInfoWrapper(dispatcher: jasmine.Spy): EventInfoWrapper { return new EventInfoWrapper(dispatcher.calls.mostRecent().args[0]); } @@ -170,6 +268,50 @@ function dispatchMouseEvent( return event; } +function dispatchKeyboardEvent( + target: Element, + { + type = 'keydown', + key = '', + location = 0, + ctrlKey = false, + altKey = false, + shiftKey = false, + metaKey = false, + }: { + type?: string; + key?: string; + location?: number; + ctrlKey?: boolean; + altKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; + } = {}, +) { + // createEvent/initKeyboardEvent is used to support IE11 + // tslint:disable:deprecation + const event = document.createEvent('KeyboardEvent'); + event.initKeyboardEvent( + type, + true, + true, + window, + key, + location, + ctrlKey, + altKey, + shiftKey, + metaKey, + ); + // tslint:enable:deprecation + // This is necessary as Chrome does not respect the key parameter in + // `initKeyboardEvent`. + Object.defineProperty(event, 'key', {value: key}); + spyOn(event, 'preventDefault').and.callThrough(); + target.dispatchEvent(event); + return event; +} + describe('EventContract', () => { beforeEach(() => { safeElement.setInnerHtml(document.body, testonlyHtml(domContent)); @@ -187,7 +329,7 @@ describe('EventContract', () => { const addEventListenerSpy2 = spyOn(container2, 'addEventListener'); const eventContractContainerManager = new EventContractMultiContainer(); - const eventContract = createEventContract({eventContractContainerManager, eventTypes: []}); + const eventContract = new EventContract(eventContractContainerManager); eventContract.addEvent('click'); expect(addEventListenerSpy).not.toHaveBeenCalled(); @@ -213,7 +355,7 @@ describe('EventContract', () => { const addEventListenerSpy2 = spyOn(container2, 'addEventListener'); const eventContractContainerManager = new EventContractMultiContainer(); - const eventContract = createEventContract({eventContractContainerManager, eventTypes: []}); + const eventContract = new EventContract(eventContractContainerManager); eventContractContainerManager.addContainer(container); eventContractContainerManager.addContainer(container2); @@ -238,7 +380,7 @@ describe('EventContract', () => { const addEventListenerSpy = spyOn(container, 'addEventListener'); const eventContractContainerManager = new EventContractMultiContainer(); - const eventContract = createEventContract({eventContractContainerManager, eventTypes: []}); + const eventContract = new EventContract(eventContractContainerManager); eventContract.addEvent('animationend', 'webkitanimationend'); eventContractContainerManager.addContainer(container); @@ -250,6 +392,7 @@ describe('EventContract', () => { it('queues events until dispatcher is registered', () => { const container = getRequiredElementById('click-container'); + const actionElement = getRequiredElementById('click-action-element'); const targetElement = getRequiredElementById('click-target-element'); const eventContract = createEventContract({ @@ -259,7 +402,7 @@ describe('EventContract', () => { const clickEvent = dispatchMouseEvent(targetElement); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); eventContract.registerDispatcher(dispatcher, Restriction.I_AM_THE_JSACTION_FRAMEWORK); expect(dispatcher).toHaveBeenCalledTimes(1); @@ -269,15 +412,17 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe(EventType.CLICK); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); expect(eventInfoWrapper.getIsReplay()).toBe(true); }); it('dispatches event', () => { const container = getRequiredElementById('click-container'); + const actionElement = getRequiredElementById('click-action-element'); const targetElement = getRequiredElementById('click-target-element'); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['click'], @@ -293,7 +438,80 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe(EventType.CLICK); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + expect(eventInfoWrapper.getResolved()).toBe(true); + }); + + it('dispatches event when targetElement is actionElement', () => { + const container = getRequiredElementById('self-click-container'); + const targetElement = getRequiredElementById('self-click-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + const clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe(EventType.CLICK); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(targetElement); + }); + + it('dispatch event to child and ignore parent', () => { + const container = getRequiredElementById('parent-and-child-container'); + const actionElement = getRequiredElementById('parent-and-child-action-element'); + const targetElement = getRequiredElementById('parent-and-child-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + const clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('childHandleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('dispatch event through owner', () => { + const container = getRequiredElementById('owner-click-container'); + const actionElement = getRequiredElementById('owner-click-action-element'); + const targetElement = getRequiredElementById('owner-click-target-element'); + targetElement[OWNER] = actionElement; + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + const clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('ownerHandleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); }); it('dispatches event for `webkitanimationend` alias event type', () => { @@ -302,9 +520,10 @@ describe('EventContract', () => { return; } const container = getRequiredElementById('animationend-container'); + const actionElement = getRequiredElementById('animationend-action-element'); const targetElement = getRequiredElementById('animationend-target-element'); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: [['animationend', 'webkitanimationend']], @@ -323,15 +542,234 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('animationend'); expect(eventInfoWrapper.getEvent()).toBe(animationEndEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleAnimationEnd'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('dispatches modified click event', () => { + const container = getRequiredElementById('clickmod-container'); + const actionElement = getRequiredElementById('clickmod-action-element'); + const targetElement = getRequiredElementById('clickmod-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + const clickEvent = dispatchMouseEvent(targetElement, {shiftKey: true}); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('clickmod'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClickMod'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('caches jsaction attribute', () => { + const container = getRequiredElementById('click-container'); + const actionElement = getRequiredElementById('click-action-element'); + const targetElement = getRequiredElementById('click-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + let clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + let eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + + actionElement.setAttribute('jsaction', 'renamedHandleClick'); + dispatcher.calls.reset(); + + clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('re-parses jsaction attribute if the action cache is cleared', () => { + const container = getRequiredElementById('click-container'); + const actionElement = getRequiredElementById('click-action-element'); + const targetElement = getRequiredElementById('click-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + let clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + let eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + + actionElement.setAttribute('jsaction', 'renamedHandleClick'); + // Clear attribute cache. + cache.clear(actionElement); + dispatcher.calls.reset(); + + clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('renamedHandleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('handles trailing semicolon in jsaction attribute', () => { + const container = getRequiredElementById('trailing-semicolon-container'); + const actionElement = getRequiredElementById('trailing-semicolon-action-element'); + const targetElement = getRequiredElementById('trailing-semicolon-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + const clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('handles jsaction attributes without action names, first action', () => { + const container = getRequiredElementById('no-action-name-container'); + const actionElement = getRequiredElementById('no-action-name-action-element'); + const targetElement = getRequiredElementById('no-action-name-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click', 'keydown', 'keyup'], + dispatcher, + }); + + const keydownEvent = dispatchKeyboardEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('keydown'); + expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe(''); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('handles jsaction attributes without action names, last action', () => { + const container = getRequiredElementById('no-action-name-container'); + const actionElement = getRequiredElementById('no-action-name-action-element'); + const targetElement = getRequiredElementById('no-action-name-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click', 'keydown', 'keyup'], + dispatcher, + }); + + const keyupEvent = dispatchKeyboardEvent(targetElement, {type: 'keyup'}); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('keyup'); + expect(eventInfoWrapper.getEvent()).toBe(keyupEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe(''); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('does not handle jsaction attributes without event type or action name', () => { + const container = getRequiredElementById('no-action-name-container'); + const targetElement = getRequiredElementById('no-action-name-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click', 'keydown', 'keyup'], + dispatcher, + }); + + const clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); expect(eventInfoWrapper.getAction()).toBeUndefined(); }); + it('dispatches event from shadow dom', () => { + const container = getRequiredElementById('shadow-dom-container'); + const actionElement = getRequiredElementById('shadow-dom-action-element'); + + // Not supported in ie11. + if (!actionElement.attachShadow) { + return; + } + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + const shadow = actionElement.attachShadow({mode: 'open'}); + const shadowChild = document.createElement('div'); + shadow.appendChild(shadowChild); + + shadowChild.click(); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + // Target element is set to the host from the event. + expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + it('cleanUp removes all event listeners and containers', () => { const container = getRequiredElementById('click-container'); const removeEventListenerSpy = spyOn(container, 'removeEventListener').and.callThrough(); const actionElement = getRequiredElementById('click-action-element'); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContractContainerManager = new EventContractContainer(container); const cleanUpSpy = spyOn(eventContractContainerManager, 'cleanUp').and.callThrough(); const eventContract = createEventContract({ @@ -354,9 +792,10 @@ describe('EventContract', () => { it('exposes event handlers with `handler()`', () => { const container = getRequiredElementById('click-container'); + const actionElement = getRequiredElementById('click-action-element'); const targetElement = getRequiredElementById('click-target-element'); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['click'], @@ -377,7 +816,8 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe(EventType.CLICK); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); }); it('has no event handlers with `handler()` for unregistered event type', () => { @@ -405,23 +845,239 @@ describe('EventContract', () => { expect(clickEvent.preventDefault).not.toHaveBeenCalled(); }); + describe('a11y click', () => { + beforeEach(() => { + EventContract.A11Y_CLICK_SUPPORT = true; + }); + + it('dispatches keydown as click event', () => { + const container = getRequiredElementById('a11y-click-container'); + const actionElement = getRequiredElementById('a11y-click-action-element'); + const targetElement = getRequiredElementById('a11y-click-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'}); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('dispatches keydown event', () => { + const container = getRequiredElementById('keydown-container'); + const actionElement = getRequiredElementById('keydown-action-element'); + const targetElement = getRequiredElementById('keydown-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['keydown'], + dispatcher, + }); + + const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'a'}); + + expect(dispatcher).toHaveBeenCalledTimes(1); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('keydown'); + expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleKeydown'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('dispatches clickonly event', () => { + const container = getRequiredElementById('a11y-clickonly-container'); + const actionElement = getRequiredElementById('a11y-clickonly-action-element'); + const targetElement = getRequiredElementById('a11y-clickonly-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + const clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('clickonly'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClickOnly'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('dispatches click event to click handler rather than clickonly', () => { + const container = getRequiredElementById('a11y-click-clickonly-container'); + const actionElement = getRequiredElementById('a11y-click-clickonly-action-element'); + const targetElement = getRequiredElementById('a11y-click-clickonly-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['click'], + dispatcher, + }); + + const clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + }); + + describe('a11y click support deferred', () => { + it('dispatches keydown as click event', () => { + const container = getRequiredElementById('a11y-click-container'); + const actionElement = getRequiredElementById('a11y-click-action-element'); + const targetElement = getRequiredElementById('a11y-click-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + const eventContract = createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + exportAddA11yClickSupport: true, + eventTypes: ['click'], + dispatcher, + }); + addDeferredA11yClickSupport(eventContract); + + const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'}); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('dispatches keydown event', () => { + const container = getRequiredElementById('keydown-container'); + const actionElement = getRequiredElementById('keydown-action-element'); + const targetElement = getRequiredElementById('keydown-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + const eventContract = createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + exportAddA11yClickSupport: true, + eventTypes: ['keydown'], + dispatcher, + }); + addDeferredA11yClickSupport(eventContract); + + const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'a'}); + + expect(dispatcher).toHaveBeenCalledTimes(1); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('keydown'); + expect(eventInfoWrapper.getEvent()).toBe(keydownEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleKeydown'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('dispatches clickonly event', () => { + const container = getRequiredElementById('a11y-clickonly-container'); + const actionElement = getRequiredElementById('a11y-clickonly-action-element'); + const targetElement = getRequiredElementById('a11y-clickonly-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + const eventContract = createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + exportAddA11yClickSupport: true, + eventTypes: ['click'], + dispatcher, + }); + addDeferredA11yClickSupport(eventContract); + + const clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('clickonly'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClickOnly'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('dispatches click event to click handler rather than clickonly', () => { + const container = getRequiredElementById('a11y-click-clickonly-container'); + const actionElement = getRequiredElementById('a11y-click-clickonly-action-element'); + const targetElement = getRequiredElementById('a11y-click-clickonly-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + const eventContract = createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + exportAddA11yClickSupport: true, + eventTypes: ['click'], + dispatcher, + }); + addDeferredA11yClickSupport(eventContract); + + const clickEvent = dispatchMouseEvent(targetElement); + + expect(dispatcher).toHaveBeenCalledTimes(1); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('click'); + expect(eventInfoWrapper.getEvent()).toBe(clickEvent); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + }); + describe('nested containers', () => { let outerContainer: Element; + let outerActionElement: Element; let outerTargetElement: Element; let innerContainer: Element; + let innerActionElement: Element; let innerTargetElement: Element; beforeEach(() => { outerContainer = getRequiredElementById('nested-outer-container'); + outerActionElement = getRequiredElementById('nested-outer-action-element'); outerTargetElement = getRequiredElementById('nested-outer-target-element'); innerContainer = getRequiredElementById('nested-inner-container'); + innerActionElement = getRequiredElementById('nested-inner-action-element'); innerTargetElement = getRequiredElementById('nested-inner-target-element'); }); it('dispatches events in outer container', () => { const documentListener = jasmine.createSpy('documentListener'); window.document.documentElement.addEventListener('click', documentListener); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContractContainerManager = createEventContractMultiContainer(outerContainer); createEventContract({ eventContractContainerManager, @@ -437,14 +1093,16 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(outerTargetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('outerHandleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(outerActionElement); + expect(documentListener).toHaveBeenCalledTimes(1); }); it('dispatches events in inner container', () => { const documentListener = jasmine.createSpy('documentListener'); window.document.documentElement.addEventListener('click', documentListener); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContractContainerManager = createEventContractMultiContainer(outerContainer); createEventContract({ eventContractContainerManager, @@ -460,14 +1118,16 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(innerTargetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('innerHandleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(innerActionElement); + expect(documentListener).toHaveBeenCalledTimes(1); }); it('dispatches events in outer container, inner registered first', () => { const documentListener = jasmine.createSpy('documentListener'); window.document.documentElement.addEventListener('click', documentListener); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContractContainerManager = createEventContractMultiContainer(innerContainer); createEventContract({ eventContractContainerManager, @@ -483,7 +1143,8 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(outerTargetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('outerHandleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(outerActionElement); expect(documentListener).toHaveBeenCalledTimes(1); }); @@ -491,7 +1152,7 @@ describe('EventContract', () => { it('dispatches events in inner container, inner container registered first', () => { const documentListener = jasmine.createSpy('documentListener'); window.document.documentElement.addEventListener('click', documentListener); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContractContainerManager = createEventContractMultiContainer(innerContainer); createEventContract({ eventContractContainerManager, @@ -507,7 +1168,8 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(innerTargetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('innerHandleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(innerActionElement); expect(documentListener).toHaveBeenCalledTimes(1); }); @@ -515,7 +1177,7 @@ describe('EventContract', () => { it('dispatches events in inner container, inner container removed', () => { const documentListener = jasmine.createSpy('documentListener'); window.document.documentElement.addEventListener('click', documentListener); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContractContainerManager = createEventContractMultiContainer(outerContainer); createEventContract({ eventContractContainerManager, @@ -532,7 +1194,8 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(innerTargetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('innerHandleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(innerActionElement); expect(documentListener).toHaveBeenCalledTimes(1); @@ -548,21 +1211,246 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(innerTargetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('innerHandleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(innerActionElement); expect(documentListener).toHaveBeenCalledTimes(1); }); }); - describe('early events', () => { - it('early events are dispatched', () => { - const container = getRequiredElementById('click-container'); - const targetElement = getRequiredElementById('click-target-element'); + describe('non-bubbling mouse events', () => { + beforeEach(() => { + EventContract.MOUSE_SPECIAL_SUPPORT = true; + }); - const removeEventListenerSpy = spyOn( + it('dispatches matching mouseover as mouseenter event', () => { + const container = getRequiredElementById('mouseenter-container'); + const actionElement = getRequiredElementById('mouseenter-action-element'); + const targetElement = getRequiredElementById('mouseenter-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['mouseenter'], + dispatcher, + }); + + dispatchMouseEvent(targetElement, { + type: 'mouseover', + // Indicates that the mouse exited the container and entered the + // target element. + relatedTarget: container, + }); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('mouseenter'); + const syntheticMouseEvent = eventInfoWrapper.getEvent(); + expect(syntheticMouseEvent.type).toBe('mouseenter'); + expect(syntheticMouseEvent.target).toBe(actionElement); + expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleMouseEnter'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('does not dispatch non-matching mouseover event as mouseenter', () => { + const container = getRequiredElementById('mouseenter-container'); + const actionElement = getRequiredElementById('mouseenter-action-element'); + const targetElement = getRequiredElementById('mouseenter-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['mouseenter'], + dispatcher, + }); + + dispatchMouseEvent(targetElement, { + type: 'mouseover', + // Indicates that the mouse exited the action element and entered the + // target element. + relatedTarget: actionElement, + }); + + // Global dispatch for the mouseover event still happens. + expect(dispatcher).toHaveBeenCalledTimes(1); + }); + + it('dispatches matching mouseout as mouseleave event', () => { + const container = getRequiredElementById('mouseleave-container'); + const actionElement = getRequiredElementById('mouseleave-action-element'); + const targetElement = getRequiredElementById('mouseleave-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['mouseleave'], + dispatcher, + }); + + dispatchMouseEvent(targetElement, { + type: 'mouseout', + // Indicates that the mouse entered the container and exited the + // target element. + relatedTarget: container, + }); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('mouseleave'); + const syntheticMouseEvent = eventInfoWrapper.getEvent(); + expect(syntheticMouseEvent.type).toBe('mouseleave'); + expect(syntheticMouseEvent.target).toBe(actionElement); + expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleMouseLeave'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('does not dispatch non-matching mouseout event as mouseleave', () => { + const container = getRequiredElementById('mouseleave-container'); + const actionElement = getRequiredElementById('mouseleave-action-element'); + const targetElement = getRequiredElementById('mouseleave-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['mouseleave'], + dispatcher, + }); + + dispatchMouseEvent(targetElement, { + type: 'mouseout', + // Indicates that the mouse entered the action element and exited the + // target element. + relatedTarget: actionElement, + }); + + // Global dispatch for the mouseout event still happens. + expect(dispatcher).toHaveBeenCalledTimes(1); + }); + + it('dispatches matching pointerover as pointerenter event', () => { + const container = getRequiredElementById('pointerenter-container'); + const actionElement = getRequiredElementById('pointerenter-action-element'); + const targetElement = getRequiredElementById('pointerenter-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['pointerenter'], + dispatcher, + }); + + dispatchMouseEvent(targetElement, { + type: 'pointerover', + // Indicates that the pointer exited the container and entered the + // target element. + relatedTarget: container, + }); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('pointerenter'); + const syntheticMouseEvent = eventInfoWrapper.getEvent(); + expect(syntheticMouseEvent.type).toBe('pointerenter'); + expect(syntheticMouseEvent.target).toBe(actionElement); + expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handlePointerEnter'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('does not dispatch non-matching pointerover event as pointerenter', () => { + const container = getRequiredElementById('pointerenter-container'); + const actionElement = getRequiredElementById('pointerenter-action-element'); + const targetElement = getRequiredElementById('pointerenter-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['pointerenter'], + dispatcher, + }); + + dispatchMouseEvent(targetElement, { + type: 'pointerover', + // Indicates that the pointer exited the action element and entered the + // target element. + relatedTarget: actionElement, + }); + + // Global dispatch for the pointerover event still happens. + expect(dispatcher).toHaveBeenCalledTimes(1); + }); + + it('dispatches matching pointerout as pointerleave event', () => { + const container = getRequiredElementById('pointerleave-container'); + const actionElement = getRequiredElementById('pointerleave-action-element'); + const targetElement = getRequiredElementById('pointerleave-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['pointerleave'], + dispatcher, + }); + + dispatchMouseEvent(targetElement, { + type: 'pointerout', + // Indicates that the pointer entered the container and exited the + // target element. + relatedTarget: container, + }); + + expect(dispatcher).toHaveBeenCalledTimes(1); + const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); + expect(eventInfoWrapper.getEventType()).toBe('pointerleave'); + const syntheticMouseEvent = eventInfoWrapper.getEvent(); + expect(syntheticMouseEvent.type).toBe('pointerleave'); + expect(syntheticMouseEvent.target).toBe(actionElement); + expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handlePointerLeave'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + }); + + it('does not dispatch non-matching pointerout event as pointerleave', () => { + const container = getRequiredElementById('pointerleave-container'); + const actionElement = getRequiredElementById('pointerleave-action-element'); + const targetElement = getRequiredElementById('pointerleave-target-element'); + + const dispatcher = jasmine.createSpy('dispatcher'); + createEventContract({ + eventContractContainerManager: new EventContractContainer(container), + eventTypes: ['pointerleave'], + dispatcher, + }); + + dispatchMouseEvent(targetElement, { + type: 'pointerout', + // Indicates that the pointer entered the action element and exited the + // target element. + relatedTarget: actionElement, + }); + + // Global dispatch for the pointerout event still happens. + expect(dispatcher).toHaveBeenCalledTimes(1); + }); + }); + + describe('early events', () => { + let removeEventListenerSpy: jasmine.Spy; + + beforeEach(() => { + removeEventListenerSpy = spyOn( window.document.documentElement, 'removeEventListener', ).and.callThrough(); + }); + + it('early events are dispatched', () => { + const container = getRequiredElementById('click-container'); + const actionElement = getRequiredElementById('click-action-element'); + const targetElement = getRequiredElementById('click-target-element'); + const earlyEventContract = new EarlyEventContract(); earlyEventContract.addEvents(['click']); @@ -573,7 +1461,7 @@ describe('EventContract', () => { expect(earlyJsactionData!.q.length).toBe(1); expect(earlyJsactionData!.q[0].event).toBe(clickEvent); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['click'], @@ -589,11 +1477,13 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); }); it('early capture events are dispatched', () => { const container = getRequiredElementById('focus-container'); + const actionElement = getRequiredElementById('focus-action-element'); const targetElement = getRequiredElementById('focus-target-element'); const replaySink = {_ejsa: undefined}; const removeEventListenerSpy = spyOn(container, 'removeEventListener').and.callThrough(); @@ -608,7 +1498,7 @@ describe('EventContract', () => { expect(earlyJsactionData!.q.length).toBe(1); expect(earlyJsactionData!.q[0].event.type).toBe('focus'); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['focus'], @@ -624,17 +1514,15 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('focus'); expect(eventInfoWrapper.getEvent().type).toBe('focus'); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('handleFocus'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); }); it('early events are dispatched when target is cleared', () => { const container = getRequiredElementById('click-container'); + const actionElement = getRequiredElementById('click-action-element'); const targetElement = getRequiredElementById('click-target-element'); - const removeEventListenerSpy = spyOn( - window.document.documentElement, - 'removeEventListener', - ).and.callThrough(); const earlyEventContract = new EarlyEventContract(); earlyEventContract.addEvents(['click']); @@ -648,7 +1536,7 @@ describe('EventContract', () => { // Emulating browser behavior of clearing target after dispatch. Object.defineProperty(clickEvent, 'target', {value: null}); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['click'], @@ -664,25 +1552,20 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(eventInfoWrapper.getAction()?.name).toBe('handleClick'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); }); describe('non-bubbling mouse events', () => { beforeEach(() => { EventContract.MOUSE_SPECIAL_SUPPORT = true; }); - afterEach(() => { - EventContract.MOUSE_SPECIAL_SUPPORT = false; - }); it('early mouseout dispatched as mouseleave and mouseout', () => { const container = getRequiredElementById('mouseleave-container'); + const actionElement = getRequiredElementById('mouseleave-action-element'); const targetElement = getRequiredElementById('mouseleave-target-element'); - const removeEventListenerSpy = spyOn( - window.document.documentElement, - 'removeEventListener', - ).and.callThrough(); const early = new EarlyEventContract(); early.addEvents(['mouseout']); @@ -700,7 +1583,7 @@ describe('EventContract', () => { expect(earlyJsactionData!.q.length).toBe(1); expect(earlyJsactionData!.q[0].event).toBe(mouseOutEvent); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['mouseout', 'mouseleave'], @@ -715,20 +1598,18 @@ describe('EventContract', () => { const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); expect(eventInfoWrapper.getEventType()).toBe('mouseleave'); const syntheticMouseEvent = eventInfoWrapper.getEvent(); - expect(syntheticMouseEvent.type).toBe('mouseout'); - expect(syntheticMouseEvent.target).toBe(targetElement); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(syntheticMouseEvent.type).toBe('mouseleave'); + expect(syntheticMouseEvent.target).toBe(actionElement); + expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleMouseLeave'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); }); it('early mouseout dispatched as only mouseleave', () => { const container = getRequiredElementById('mouseleave-container'); + const actionElement = getRequiredElementById('mouseleave-action-element'); const targetElement = getRequiredElementById('mouseleave-target-element'); - const removeEventListenerSpy = spyOn( - window.document.documentElement, - 'removeEventListener', - ).and.callThrough(); const early = new EarlyEventContract(); early.addEvents(['mouseout']); @@ -744,7 +1625,7 @@ describe('EventContract', () => { expect(earlyJsactionData!.q.length).toBe(1); expect(earlyJsactionData!.q[0].event).toBe(mouseOutEvent); - const dispatcher = createDispatcherSpy(); + const dispatcher = jasmine.createSpy('dispatcher'); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['mouseleave'], @@ -759,10 +1640,11 @@ describe('EventContract', () => { const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher); expect(eventInfoWrapper.getEventType()).toBe('mouseleave'); const syntheticMouseEvent = eventInfoWrapper.getEvent(); - expect(syntheticMouseEvent.type).toBe('mouseout'); - expect(syntheticMouseEvent.target).toBe(targetElement); - expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()).toBeUndefined(); + expect(syntheticMouseEvent.type).toBe('mouseleave'); + expect(syntheticMouseEvent.target).toBe(actionElement); + expect(eventInfoWrapper.getTargetElement()).toBe(actionElement); + expect(eventInfoWrapper.getAction()?.name).toBe('handleMouseLeave'); + expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); }); }); }); diff --git a/packages/core/src/event_delegation_utils.ts b/packages/core/src/event_delegation_utils.ts index 35a773f8440..142f8d9f1f9 100644 --- a/packages/core/src/event_delegation_utils.ts +++ b/packages/core/src/event_delegation_utils.ts @@ -78,10 +78,7 @@ export const initGlobalEventDelegation = ( if (injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT)) { return; } - eventDelegation.eventContract = new EventContract( - new EventContractContainer(document.body), - /* useActionResolver= */ false, - ); + eventDelegation.eventContract = new EventContract(new EventContractContainer(document.body)); const dispatcher = new EventDispatcher(invokeRegisteredListeners); registerDispatcher(eventDelegation.eventContract, dispatcher); }; diff --git a/packages/core/src/hydration/event_replay.ts b/packages/core/src/hydration/event_replay.ts index c3d36d11ebe..e611742c783 100644 --- a/packages/core/src/hydration/event_replay.ts +++ b/packages/core/src/hydration/event_replay.ts @@ -118,7 +118,6 @@ const initEventReplay = (eventDelegation: GlobalEventDelegation, injector: Injec const earlyJsactionData = getJsactionData(container)!; const eventContract = (eventDelegation.eventContract = new EventContract( new EventContractContainer(earlyJsactionData.c), - /* useActionResolver= */ false, )); for (const et of earlyJsactionData.et) { eventContract.addEvent(et);