Skip to content

Commit 33a6686

Browse files
tbondwilkinsonthePunderWoman
authored andcommitted
refactor(core): Add and move tests for Dispatcher. (#56193)
Move tests from `eventcontract_test.ts` to `dispatcher_test.ts` for functionality that lives in `Dispatcher`. Add an extra test for `preventDefault` behavior for `CLICKMOD` that covers a previous bug case. PR Close #56193
1 parent b283c0c commit 33a6686

File tree

2 files changed

+327
-120
lines changed

2 files changed

+327
-120
lines changed

packages/core/primitives/event-dispatch/test/dispatcher_test.ts

Lines changed: 326 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,160 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Dispatcher, Replayer} from '../src/dispatcher';
10-
import {ActionInfo, createEventInfo, EventInfo, EventInfoWrapper} from '../src/event_info';
9+
import {ActionInfo, createEventInfo, EventInfoWrapper} from '../src/event_info';
10+
import {Dispatcher, registerDispatcher, Replayer} from '../src/dispatcher';
11+
import {addDeferredA11yClickSupport, EventContract} from '../src/eventcontract';
12+
import {EventContractContainer} from '../src/event_contract_container';
13+
import {safeElement, testonlyHtml} from './html';
14+
15+
const domContent = `
16+
<div id="anchor-click-container">
17+
<a id="anchor-click-action-element" href="javascript:void(0);" jsaction="handleClick">
18+
<span id="anchor-click-target-element"></span>
19+
</a>
20+
</div>
21+
22+
<div id="anchor-clickmod-container">
23+
<a id="anchor-clickmod-action-element" href="javascript:void(0);" jsaction="clickmod: handleClickMod">
24+
<span id="anchor-clickmod-target-element"></span>
25+
</a>
26+
</div>
27+
28+
<div id="clickmod-container">
29+
<div id="clickmod-action-element" jsaction="clickmod:handleClickMod">
30+
<div id="clickmod-target-element"></div>
31+
</div>
32+
</div>
33+
34+
<div id="a11y-anchor-click-container">
35+
<a id="a11y-anchor-click-action-element" href="javascript:void(0);" jsaction="handleClick">
36+
<span id="a11y-anchor-click-target-element" tabindex="0"></span>
37+
</a>
38+
</div>
39+
`;
40+
41+
function getRequiredElementById(id: string) {
42+
const element = document.getElementById(id);
43+
expect(element).not.toBeNull();
44+
return element!;
45+
}
46+
47+
function createEventContract({
48+
container,
49+
eventTypes,
50+
exportAddA11yClickSupport = false,
51+
}: {
52+
container: Element;
53+
eventTypes: Array<string | [string, string]>;
54+
exportAddA11yClickSupport?: boolean;
55+
}): EventContract {
56+
const eventContract = new EventContract(new EventContractContainer(container));
57+
if (exportAddA11yClickSupport) {
58+
eventContract.exportAddA11yClickSupport();
59+
}
60+
for (const eventType of eventTypes) {
61+
if (typeof eventType === 'string') {
62+
eventContract.addEvent(eventType);
63+
} else {
64+
const [aliasedEventType, aliasEventType] = eventType;
65+
eventContract.addEvent(aliasedEventType, aliasEventType);
66+
}
67+
}
68+
return eventContract;
69+
}
1170

1271
function createClickEvent() {
1372
return new MouseEvent('click', {bubbles: true, cancelable: true});
1473
}
1574

75+
function dispatchMouseEvent(
76+
target: Element,
77+
{
78+
type = 'click',
79+
ctrlKey = false,
80+
altKey = false,
81+
shiftKey = false,
82+
metaKey = false,
83+
relatedTarget = null,
84+
}: {
85+
type?: string;
86+
ctrlKey?: boolean;
87+
altKey?: boolean;
88+
shiftKey?: boolean;
89+
metaKey?: boolean;
90+
relatedTarget?: Element | null;
91+
} = {},
92+
) {
93+
// createEvent/initMouseEvent is used to support IE11
94+
// tslint:disable:deprecation
95+
const event = document.createEvent('MouseEvent');
96+
event.initMouseEvent(
97+
type,
98+
true,
99+
true,
100+
window,
101+
0,
102+
0,
103+
0,
104+
0,
105+
0,
106+
ctrlKey,
107+
altKey,
108+
shiftKey,
109+
metaKey,
110+
0,
111+
relatedTarget,
112+
);
113+
// tslint:enable:deprecation
114+
spyOn(event, 'preventDefault').and.callThrough();
115+
target.dispatchEvent(event);
116+
return event;
117+
}
118+
119+
function dispatchKeyboardEvent(
120+
target: Element,
121+
{
122+
type = 'keydown',
123+
key = '',
124+
location = 0,
125+
ctrlKey = false,
126+
altKey = false,
127+
shiftKey = false,
128+
metaKey = false,
129+
}: {
130+
type?: string;
131+
key?: string;
132+
location?: number;
133+
ctrlKey?: boolean;
134+
altKey?: boolean;
135+
shiftKey?: boolean;
136+
metaKey?: boolean;
137+
} = {},
138+
) {
139+
// createEvent/initKeyboardEvent is used to support IE11
140+
// tslint:disable:deprecation
141+
const event = document.createEvent('KeyboardEvent');
142+
event.initKeyboardEvent(
143+
type,
144+
true,
145+
true,
146+
window,
147+
key,
148+
location,
149+
ctrlKey,
150+
altKey,
151+
shiftKey,
152+
metaKey,
153+
);
154+
// tslint:enable:deprecation
155+
// This is necessary as Chrome does not respect the key parameter in
156+
// `initKeyboardEvent`.
157+
Object.defineProperty(event, 'key', {value: key});
158+
spyOn(event, 'preventDefault').and.callThrough();
159+
target.dispatchEvent(event);
160+
return event;
161+
}
162+
16163
function createTestActionInfo({
17164
name = 'handleClick',
18165
element = document.createElement('div'),
@@ -50,7 +197,14 @@ function createTestEventInfoWrapper({
50197
);
51198
}
52199

53-
describe('dispatcher test.ts', () => {
200+
describe('Dispatcher', () => {
201+
beforeEach(() => {
202+
safeElement.setInnerHtml(document.body, testonlyHtml(domContent));
203+
204+
// Normalize timestamp.
205+
spyOn(Date, 'now').and.returnValue(0);
206+
});
207+
54208
it('dispatches to dispatchDelegate', () => {
55209
const dispatchDelegate =
56210
jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatchDelegate');
@@ -104,4 +258,173 @@ describe('dispatcher test.ts', () => {
104258
expect(dispatchDelegate).toHaveBeenCalledTimes(0);
105259
expect(eventReplayer).toHaveBeenCalledWith(eventInfoWrappers);
106260
});
261+
262+
it('prevents default for click on anchor child', () => {
263+
const container = getRequiredElementById('anchor-click-container');
264+
const actionElement = getRequiredElementById('anchor-click-action-element');
265+
const targetElement = getRequiredElementById('anchor-click-target-element');
266+
267+
const eventContract = createEventContract({
268+
container,
269+
eventTypes: ['click'],
270+
});
271+
const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch');
272+
const dispatcher = new Dispatcher(dispatch);
273+
registerDispatcher(eventContract, dispatcher);
274+
275+
const clickEvent = dispatchMouseEvent(targetElement);
276+
277+
expect(dispatch).toHaveBeenCalledTimes(1);
278+
const eventInfoWrapper = dispatch.calls.mostRecent().args[0];
279+
expect(eventInfoWrapper.getEventType()).toBe('click');
280+
expect(eventInfoWrapper.getEvent()).toBe(clickEvent);
281+
expect(eventInfoWrapper.getTargetElement()).toBe(targetElement);
282+
expect(eventInfoWrapper.getAction()?.name).toBe('handleClick');
283+
expect(eventInfoWrapper.getAction()?.element).toBe(actionElement);
284+
285+
expect(clickEvent.preventDefault).toHaveBeenCalled();
286+
});
287+
288+
it('prevents default for modified click on anchor child', () => {
289+
const container = getRequiredElementById('anchor-clickmod-container');
290+
const actionElement = getRequiredElementById('anchor-clickmod-action-element');
291+
const targetElement = getRequiredElementById('anchor-clickmod-target-element');
292+
293+
const eventContract = createEventContract({
294+
container,
295+
eventTypes: ['click'],
296+
});
297+
const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch');
298+
const dispatcher = new Dispatcher(dispatch);
299+
registerDispatcher(eventContract, dispatcher);
300+
301+
const clickEvent = dispatchMouseEvent(targetElement, {shiftKey: true});
302+
303+
expect(dispatch).toHaveBeenCalledTimes(1);
304+
const eventInfoWrapper = dispatch.calls.mostRecent().args[0];
305+
expect(eventInfoWrapper.getEventType()).toBe('clickmod');
306+
expect(eventInfoWrapper.getEvent()).toBe(clickEvent);
307+
expect(eventInfoWrapper.getTargetElement()).toBe(targetElement);
308+
expect(eventInfoWrapper.getAction()?.name).toBe('handleClickMod');
309+
expect(eventInfoWrapper.getAction()?.element).toBe(actionElement);
310+
311+
expect(clickEvent.preventDefault).toHaveBeenCalled();
312+
});
313+
314+
it('does not prevent default for modified click on non-anchor child', () => {
315+
const container = getRequiredElementById('clickmod-container');
316+
const actionElement = getRequiredElementById('clickmod-action-element');
317+
const targetElement = getRequiredElementById('clickmod-target-element');
318+
319+
const eventContract = createEventContract({
320+
container,
321+
eventTypes: ['click'],
322+
});
323+
const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch');
324+
const dispatcher = new Dispatcher(dispatch);
325+
registerDispatcher(eventContract, dispatcher);
326+
327+
const clickEvent = dispatchMouseEvent(targetElement, {shiftKey: true});
328+
329+
expect(dispatch).toHaveBeenCalledTimes(1);
330+
const eventInfoWrapper = dispatch.calls.mostRecent().args[0];
331+
expect(eventInfoWrapper.getEventType()).toBe('clickmod');
332+
expect(eventInfoWrapper.getEvent()).toBe(clickEvent);
333+
expect(eventInfoWrapper.getTargetElement()).toBe(targetElement);
334+
expect(eventInfoWrapper.getAction()?.name).toBe('handleClickMod');
335+
expect(eventInfoWrapper.getAction()?.element).toBe(actionElement);
336+
337+
expect(clickEvent.preventDefault).not.toHaveBeenCalled();
338+
});
339+
340+
describe('a11y click', () => {
341+
beforeAll(() => {
342+
EventContract.A11Y_CLICK_SUPPORT = true;
343+
});
344+
afterAll(() => {
345+
EventContract.A11Y_CLICK_SUPPORT = false;
346+
});
347+
348+
it('prevents default for enter key on anchor child', () => {
349+
const container = getRequiredElementById('a11y-anchor-click-container');
350+
const actionElement = getRequiredElementById('a11y-anchor-click-action-element');
351+
const targetElement = getRequiredElementById('a11y-anchor-click-target-element');
352+
353+
const eventContract = createEventContract({
354+
container,
355+
eventTypes: ['click'],
356+
});
357+
const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch');
358+
const dispatcher = new Dispatcher(dispatch);
359+
registerDispatcher(eventContract, dispatcher);
360+
361+
const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'});
362+
363+
expect(dispatch).toHaveBeenCalledTimes(1);
364+
const eventInfoWrapper = dispatch.calls.mostRecent().args[0];
365+
expect(eventInfoWrapper.getEventType()).toBe('click');
366+
expect(eventInfoWrapper.getEvent()).toBe(keydownEvent);
367+
expect(eventInfoWrapper.getTargetElement()).toBe(targetElement);
368+
expect(eventInfoWrapper.getAction()?.name).toBe('handleClick');
369+
expect(eventInfoWrapper.getAction()?.element).toBe(actionElement);
370+
371+
expect(keydownEvent.preventDefault).toHaveBeenCalled();
372+
});
373+
374+
it('prevents default for enter key on anchor child', () => {
375+
const container = getRequiredElementById('a11y-anchor-click-container');
376+
const actionElement = getRequiredElementById('a11y-anchor-click-action-element');
377+
const targetElement = getRequiredElementById('a11y-anchor-click-target-element');
378+
379+
const eventContract = createEventContract({
380+
container,
381+
eventTypes: ['click'],
382+
});
383+
const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch');
384+
const dispatcher = new Dispatcher(dispatch);
385+
registerDispatcher(eventContract, dispatcher);
386+
387+
const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'});
388+
389+
expect(dispatch).toHaveBeenCalledTimes(1);
390+
const eventInfoWrapper = dispatch.calls.mostRecent().args[0];
391+
expect(eventInfoWrapper.getEventType()).toBe('click');
392+
expect(eventInfoWrapper.getEvent()).toBe(keydownEvent);
393+
expect(eventInfoWrapper.getTargetElement()).toBe(targetElement);
394+
expect(eventInfoWrapper.getAction()?.name).toBe('handleClick');
395+
expect(eventInfoWrapper.getAction()?.element).toBe(actionElement);
396+
397+
expect(keydownEvent.preventDefault).toHaveBeenCalled();
398+
});
399+
});
400+
401+
describe('a11y click support deferred', () => {
402+
it('prevents default for enter key on anchor child', () => {
403+
const container = getRequiredElementById('a11y-anchor-click-container');
404+
const actionElement = getRequiredElementById('a11y-anchor-click-action-element');
405+
const targetElement = getRequiredElementById('a11y-anchor-click-target-element');
406+
407+
const eventContract = createEventContract({
408+
container,
409+
exportAddA11yClickSupport: true,
410+
eventTypes: ['click'],
411+
});
412+
addDeferredA11yClickSupport(eventContract);
413+
const dispatch = jasmine.createSpy<(eventInfoWrapper: EventInfoWrapper) => void>('dispatch');
414+
const dispatcher = new Dispatcher(dispatch);
415+
registerDispatcher(eventContract, dispatcher);
416+
417+
const keydownEvent = dispatchKeyboardEvent(targetElement, {key: 'Enter'});
418+
419+
expect(dispatch).toHaveBeenCalledTimes(1);
420+
const eventInfoWrapper = dispatch.calls.mostRecent().args[0];
421+
expect(eventInfoWrapper.getEventType()).toBe('click');
422+
expect(eventInfoWrapper.getEvent()).toBe(keydownEvent);
423+
expect(eventInfoWrapper.getTargetElement()).toBe(targetElement);
424+
expect(eventInfoWrapper.getAction()?.name).toBe('handleClick');
425+
expect(eventInfoWrapper.getAction()?.element).toBe(actionElement);
426+
427+
expect(keydownEvent.preventDefault).toHaveBeenCalled();
428+
});
429+
});
107430
});

0 commit comments

Comments
 (0)