From 247dce0023af439dedef6fb6dfd5b648f76a2c7f Mon Sep 17 00:00:00 2001 From: Tom Wilkinson Date: Mon, 10 Jun 2024 14:24:35 -0500 Subject: [PATCH] refactor(core): Use `ActionResolver` in `Dispatcher`. (#56369) `EventContract` usages in Angular now use `false` for `useActionResolver`. Tests have been updated, with functionality that depends on `ActionResolver` moving to dispatcher_test.ts. PR Close #56369 --- .../primitives/event-dispatch/index.api.md | 2 +- .../event-dispatch/src/eventcontract.ts | 2 +- .../event-dispatch/test/dispatcher_test.ts | 874 ++++++++++++-- .../event-dispatch/test/eventcontract_test.ts | 1014 ++--------------- packages/core/src/event_delegation_utils.ts | 5 +- packages/core/src/hydration/event_replay.ts | 1 + 6 files changed, 882 insertions(+), 1016 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 3d69b9de0b6..e9fd5f3aad6 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?: boolean); + constructor(containerManager: EventContractContainerManager, useActionResolver: false); // (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 53c969f1069..90acc044a24 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 = true, + private readonly useActionResolver: false, ) { 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 d9380abff7c..d08dc5bea4b 100644 --- a/packages/core/primitives/event-dispatch/test/dispatcher_test.ts +++ b/packages/core/primitives/event-dispatch/test/dispatcher_test.ts @@ -6,19 +6,101 @@ * 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 {addDeferredA11yClickSupport, EventContract} from '../src/eventcontract'; +import {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) { @@ -53,7 +160,10 @@ function createEventContract({ eventTypes: Array; exportAddA11yClickSupport?: boolean; }): EventContract { - const eventContract = new EventContract(new EventContractContainer(container)); + const eventContract = new EventContract( + new EventContractContainer(container), + /* useActionResolver= */ false, + ); if (exportAddA11yClickSupport) { eventContract.exportAddA11yClickSupport(); } @@ -197,6 +307,38 @@ 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)); @@ -205,21 +347,319 @@ describe('Dispatcher', () => { spyOn(Date, 'now').and.returnValue(0); }); - it('dispatches to dispatchDelegate', () => { - const dispatchDelegate = - jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate'); - const dispatcher = new Dispatcher(dispatchDelegate); - const eventInfoWrapper = createTestEventInfoWrapper(); + it('dispatches event', () => { + const container = getRequiredElementById('click-container'); + const actionElement = getRequiredElementById('click-action-element'); + const targetElement = getRequiredElementById('click-target-element'); - dispatcher.dispatch(eventInfoWrapper.eventInfo); + const eventContract = createEventContract({ + container, + eventTypes: ['click'], + }); + const dispatchDelegate = createDispatchDelegateSpy(); + createDispatcher({dispatchDelegate, eventContract}); - expect(dispatchDelegate).toHaveBeenCalledWith(eventInfoWrapper); + 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('replays to dispatchDelegate', async () => { - const dispatchDelegate = - jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate'); - const dispatcher = new Dispatcher(dispatchDelegate); + 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}); const eventInfoWrappers = [ createTestEventInfoWrapper({isReplay: true}), createTestEventInfoWrapper({isReplay: true}), @@ -230,8 +670,6 @@ 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]]); @@ -239,10 +677,9 @@ describe('Dispatcher', () => { }); it('replays to event replayer', async () => { - const dispatchDelegate = - jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate'); + const dispatchDelegate = createDispatchDelegateSpy(); const eventReplayer = jasmine.createSpy('eventReplayer'); - const dispatcher = new Dispatcher(dispatchDelegate, {eventReplayer}); + const dispatcher = createDispatcher({dispatchDelegate, eventReplayer}); const eventInfoWrappers = [ createTestEventInfoWrapper({isReplay: true}), createTestEventInfoWrapper({isReplay: true}), @@ -268,14 +705,13 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); - const dispatcher = new Dispatcher(dispatch); - registerDispatcher(eventContract, dispatcher); + const dispatchDelegate = createDispatchDelegateSpy(); + createDispatcher({dispatchDelegate, eventContract}); const clickEvent = dispatchMouseEvent(targetElement); - expect(dispatch).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; + 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); @@ -294,14 +730,14 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); - const dispatcher = new Dispatcher(dispatch); - registerDispatcher(eventContract, dispatcher); + const dispatchDelegate = + jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate'); + createDispatcher({dispatchDelegate, eventContract}); const clickEvent = dispatchMouseEvent(targetElement, {shiftKey: true}); - expect(dispatch).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; + 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); @@ -320,14 +756,13 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); - const dispatcher = new Dispatcher(dispatch); - registerDispatcher(eventContract, dispatcher); + const dispatchDelegate = createDispatchDelegateSpy(); + createDispatcher({dispatchDelegate, eventContract}); const clickEvent = dispatchMouseEvent(targetElement, {shiftKey: true}); - expect(dispatch).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; + 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); @@ -338,11 +773,103 @@ describe('Dispatcher', () => { }); describe('a11y click', () => { - beforeAll(() => { + beforeEach(() => { EventContract.A11Y_CLICK_SUPPORT = true; }); - afterAll(() => { - EventContract.A11Y_CLICK_SUPPORT = false; + 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); }); it('prevents default for enter key on anchor child', () => { @@ -354,14 +881,13 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); - const dispatcher = new Dispatcher(dispatch); - registerDispatcher(eventContract, dispatcher); + const dispatchDelegate = createDispatchDelegateSpy(); + createDispatcher({dispatchDelegate, eventContract, a11yClickSupport: true}); const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'}); - expect(dispatch).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; + 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); @@ -380,14 +906,13 @@ describe('Dispatcher', () => { container, eventTypes: ['click'], }); - const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); - const dispatcher = new Dispatcher(dispatch); - registerDispatcher(eventContract, dispatcher); + const dispatchDelegate = createDispatchDelegateSpy(); + createDispatcher({dispatchDelegate, eventContract, a11yClickSupport: true}); const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'}); - expect(dispatch).toHaveBeenCalledTimes(1); - const eventInfoWrapper = dispatch.calls.mostRecent().args[0]; + 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); @@ -398,33 +923,252 @@ describe('Dispatcher', () => { }); }); - 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'); + 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'); const eventContract = createEventContract({ container, - exportAddA11yClickSupport: true, - eventTypes: ['click'], + eventTypes: ['mouseenter'], }); - addDeferredA11yClickSupport(eventContract); - const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch'); - const dispatcher = new Dispatcher(dispatch); - registerDispatcher(eventContract, dispatcher); + const dispatchDelegate = createDispatchDelegateSpy(); + createDispatcher({dispatchDelegate, eventContract, syntheticMouseEventSupport: true}); - const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'}); + dispatchMouseEvent(targetElement, { + type: 'mouseover', + // Indicates that the mouse exited the container and entered the + // target element. + relatedTarget: container, + }); - 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()?.name).toBe('handleClick'); + 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); + }); - expect(keydownEvent.preventDefault).toHaveBeenCalled(); + 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(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()?.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(); }); }); }); diff --git a/packages/core/primitives/event-dispatch/test/eventcontract_test.ts b/packages/core/primitives/event-dispatch/test/eventcontract_test.ts index d3e050a8da5..26f75e740e5 100644 --- a/packages/core/primitives/event-dispatch/test/eventcontract_test.ts +++ b/packages/core/primitives/event-dispatch/test/eventcontract_test.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import * as cache from '../src/cache'; import { EarlyEventContract, EarlyJsactionData, @@ -19,8 +18,7 @@ import { import {EventContractMultiContainer} from '../src/event_contract_multi_container'; import {EventInfoWrapper} from '../src/event_info'; import {EventType} from '../src/event_type'; -import {addDeferredA11yClickSupport, Dispatcher, EventContract} from '../src/eventcontract'; -import {OWNER} from '../src/property'; +import {Dispatcher, EventContract} from '../src/eventcontract'; import {Restriction} from '../src/restriction'; import {safeElement, testonlyHtml} from './html'; @@ -41,98 +39,18 @@ const domContent = `
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
- -
-
-
- -
- -
-
-
- -
- - - -
-
-
- -
- -
-
-
- -
-
@@ -145,30 +63,12 @@ const domContent = `
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
@@ -193,19 +93,17 @@ function createEventContractMultiContainer( function createEventContract({ eventContractContainerManager, - exportAddA11yClickSupport = false, eventTypes, dispatcher, }: { eventContractContainerManager: EventContractContainerManager; - exportAddA11yClickSupport?: boolean; eventTypes: Array; dispatcher?: jasmine.Spy; }): EventContract { - const eventContract = new EventContract(eventContractContainerManager); - if (exportAddA11yClickSupport) { - eventContract.exportAddA11yClickSupport(); - } + const eventContract = new EventContract( + eventContractContainerManager, + /* useActionResolver= */ false, + ); for (const eventType of eventTypes) { if (typeof eventType === 'string') { eventContract.addEvent(eventType); @@ -220,6 +118,10 @@ function createEventContract({ return eventContract; } +function createDispatcherSpy() { + return jasmine.createSpy('dispatcher'); +} + function getLastDispatchedEventInfoWrapper(dispatcher: jasmine.Spy): EventInfoWrapper { return new EventInfoWrapper(dispatcher.calls.mostRecent().args[0]); } @@ -268,50 +170,6 @@ 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)); @@ -329,7 +187,7 @@ describe('EventContract', () => { const addEventListenerSpy2 = spyOn(container2, 'addEventListener'); const eventContractContainerManager = new EventContractMultiContainer(); - const eventContract = new EventContract(eventContractContainerManager); + const eventContract = createEventContract({eventContractContainerManager, eventTypes: []}); eventContract.addEvent('click'); expect(addEventListenerSpy).not.toHaveBeenCalled(); @@ -355,7 +213,7 @@ describe('EventContract', () => { const addEventListenerSpy2 = spyOn(container2, 'addEventListener'); const eventContractContainerManager = new EventContractMultiContainer(); - const eventContract = new EventContract(eventContractContainerManager); + const eventContract = createEventContract({eventContractContainerManager, eventTypes: []}); eventContractContainerManager.addContainer(container); eventContractContainerManager.addContainer(container2); @@ -380,7 +238,7 @@ describe('EventContract', () => { const addEventListenerSpy = spyOn(container, 'addEventListener'); const eventContractContainerManager = new EventContractMultiContainer(); - const eventContract = new EventContract(eventContractContainerManager); + const eventContract = createEventContract({eventContractContainerManager, eventTypes: []}); eventContract.addEvent('animationend', 'webkitanimationend'); eventContractContainerManager.addContainer(container); @@ -392,7 +250,6 @@ 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({ @@ -402,7 +259,7 @@ describe('EventContract', () => { const clickEvent = dispatchMouseEvent(targetElement); - const dispatcher = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); eventContract.registerDispatcher(dispatcher, Restriction.I_AM_THE_JSACTION_FRAMEWORK); expect(dispatcher).toHaveBeenCalledTimes(1); @@ -412,17 +269,15 @@ describe('EventContract', () => { 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(actionElement); + expect(eventInfoWrapper.getAction()).toBeUndefined(); 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 = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['click'], @@ -438,80 +293,7 @@ describe('EventContract', () => { 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(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); + expect(eventInfoWrapper.getAction()).toBeUndefined(); }); it('dispatches event for `webkitanimationend` alias event type', () => { @@ -520,10 +302,9 @@ describe('EventContract', () => { return; } const container = getRequiredElementById('animationend-container'); - const actionElement = getRequiredElementById('animationend-action-element'); const targetElement = getRequiredElementById('animationend-target-element'); - const dispatcher = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: [['animationend', 'webkitanimationend']], @@ -542,234 +323,15 @@ 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 = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContractContainerManager = new EventContractContainer(container); const cleanUpSpy = spyOn(eventContractContainerManager, 'cleanUp').and.callThrough(); const eventContract = createEventContract({ @@ -792,10 +354,9 @@ 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 = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['click'], @@ -816,8 +377,7 @@ describe('EventContract', () => { 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(actionElement); + expect(eventInfoWrapper.getAction()).toBeUndefined(); }); it('has no event handlers with `handler()` for unregistered event type', () => { @@ -845,239 +405,23 @@ 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 = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContractContainerManager = createEventContractMultiContainer(outerContainer); createEventContract({ eventContractContainerManager, @@ -1093,16 +437,14 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(outerTargetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('outerHandleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(outerActionElement); - + expect(eventInfoWrapper.getAction()).toBeUndefined(); expect(documentListener).toHaveBeenCalledTimes(1); }); it('dispatches events in inner container', () => { const documentListener = jasmine.createSpy('documentListener'); window.document.documentElement.addEventListener('click', documentListener); - const dispatcher = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContractContainerManager = createEventContractMultiContainer(outerContainer); createEventContract({ eventContractContainerManager, @@ -1118,16 +460,14 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(innerTargetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('innerHandleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(innerActionElement); - + expect(eventInfoWrapper.getAction()).toBeUndefined(); 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 = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContractContainerManager = createEventContractMultiContainer(innerContainer); createEventContract({ eventContractContainerManager, @@ -1143,8 +483,7 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(outerTargetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('outerHandleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(outerActionElement); + expect(eventInfoWrapper.getAction()).toBeUndefined(); expect(documentListener).toHaveBeenCalledTimes(1); }); @@ -1152,7 +491,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 = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContractContainerManager = createEventContractMultiContainer(innerContainer); createEventContract({ eventContractContainerManager, @@ -1168,8 +507,7 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(innerTargetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('innerHandleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(innerActionElement); + expect(eventInfoWrapper.getAction()).toBeUndefined(); expect(documentListener).toHaveBeenCalledTimes(1); }); @@ -1177,7 +515,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 = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContractContainerManager = createEventContractMultiContainer(outerContainer); createEventContract({ eventContractContainerManager, @@ -1194,8 +532,7 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(innerTargetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('innerHandleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(innerActionElement); + expect(eventInfoWrapper.getAction()).toBeUndefined(); expect(documentListener).toHaveBeenCalledTimes(1); @@ -1211,246 +548,21 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('click'); expect(eventInfoWrapper.getEvent()).toBe(clickEvent); expect(eventInfoWrapper.getTargetElement()).toBe(innerTargetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('innerHandleClick'); - expect(eventInfoWrapper.getAction()?.element).toBe(innerActionElement); + expect(eventInfoWrapper.getAction()).toBeUndefined(); expect(documentListener).toHaveBeenCalledTimes(1); }); }); - describe('non-bubbling mouse events', () => { - beforeEach(() => { - EventContract.MOUSE_SPECIAL_SUPPORT = true; - }); - - 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; + it('early events are dispatched', () => { + const container = getRequiredElementById('click-container'); + const targetElement = getRequiredElementById('click-target-element'); - beforeEach(() => { - removeEventListenerSpy = spyOn( + const 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']); @@ -1461,7 +573,7 @@ describe('EventContract', () => { expect(earlyJsactionData!.q.length).toBe(1); expect(earlyJsactionData!.q[0].event).toBe(clickEvent); - const dispatcher = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['click'], @@ -1477,13 +589,11 @@ describe('EventContract', () => { 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(eventInfoWrapper.getAction()).toBeUndefined(); }); 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(); @@ -1498,7 +608,7 @@ describe('EventContract', () => { expect(earlyJsactionData!.q.length).toBe(1); expect(earlyJsactionData!.q[0].event.type).toBe('focus'); - const dispatcher = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['focus'], @@ -1514,15 +624,17 @@ describe('EventContract', () => { expect(eventInfoWrapper.getEventType()).toBe('focus'); expect(eventInfoWrapper.getEvent().type).toBe('focus'); expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); - expect(eventInfoWrapper.getAction()?.name).toBe('handleFocus'); - expect(eventInfoWrapper.getAction()?.element).toBe(actionElement); + expect(eventInfoWrapper.getAction()).toBeUndefined(); }); 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']); @@ -1536,7 +648,7 @@ describe('EventContract', () => { // Emulating browser behavior of clearing target after dispatch. Object.defineProperty(clickEvent, 'target', {value: null}); - const dispatcher = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['click'], @@ -1552,20 +664,25 @@ describe('EventContract', () => { 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(eventInfoWrapper.getAction()).toBeUndefined(); }); 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']); @@ -1583,7 +700,7 @@ describe('EventContract', () => { expect(earlyJsactionData!.q.length).toBe(1); expect(earlyJsactionData!.q[0].event).toBe(mouseOutEvent); - const dispatcher = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['mouseout', 'mouseleave'], @@ -1598,18 +715,20 @@ describe('EventContract', () => { 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); + expect(syntheticMouseEvent.type).toBe('mouseout'); + expect(syntheticMouseEvent.target).toBe(targetElement); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()).toBeUndefined(); }); 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']); @@ -1625,7 +744,7 @@ describe('EventContract', () => { expect(earlyJsactionData!.q.length).toBe(1); expect(earlyJsactionData!.q[0].event).toBe(mouseOutEvent); - const dispatcher = jasmine.createSpy('dispatcher'); + const dispatcher = createDispatcherSpy(); const eventContract = createEventContract({ eventContractContainerManager: new EventContractContainer(container), eventTypes: ['mouseleave'], @@ -1640,11 +759,10 @@ describe('EventContract', () => { 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); + expect(syntheticMouseEvent.type).toBe('mouseout'); + expect(syntheticMouseEvent.target).toBe(targetElement); + expect(eventInfoWrapper.getTargetElement()).toBe(targetElement); + expect(eventInfoWrapper.getAction()).toBeUndefined(); }); }); }); diff --git a/packages/core/src/event_delegation_utils.ts b/packages/core/src/event_delegation_utils.ts index 142f8d9f1f9..35a773f8440 100644 --- a/packages/core/src/event_delegation_utils.ts +++ b/packages/core/src/event_delegation_utils.ts @@ -78,7 +78,10 @@ export const initGlobalEventDelegation = ( if (injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT)) { return; } - eventDelegation.eventContract = new EventContract(new EventContractContainer(document.body)); + eventDelegation.eventContract = new EventContract( + new EventContractContainer(document.body), + /* useActionResolver= */ false, + ); 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 e611742c783..c3d36d11ebe 100644 --- a/packages/core/src/hydration/event_replay.ts +++ b/packages/core/src/hydration/event_replay.ts @@ -118,6 +118,7 @@ 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);