Skip to content

refactor(cdk/testing): switch to event constructor for creating fake events #23394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cdk/a11y/fake-event-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export function isFakeMousedownFromScreenReader(event: MouseEvent): boolean {
// Some screen readers will dispatch a fake `mousedown` event when pressing enter or space on
// a clickable element. We can distinguish these events when both `offsetX` and `offsetY` are
// zero. Note that there's an edge case where the user could click the 0x0 spot of the screen
// themselves, but that is unlikely to contain interaction elements. Historially we used to check
// `event.buttons === 0`, however that no longer works on recent versions of NVDA.
// themselves, but that is unlikely to contain interaction elements. Historically we used to
// check `event.buttons === 0`, however that no longer works on recent versions of NVDA.
return event.offsetX === 0 && event.offsetY === 0;
}

Expand Down
4 changes: 2 additions & 2 deletions src/cdk/testing/testbed/fake-events/dispatch-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export function dispatchEvent<T extends Event>(node: Node | Window, event: T): T
* Shorthand to dispatch a fake event on a specified node.
* @docs-private
*/
export function dispatchFakeEvent(node: Node | Window, type: string, canBubble?: boolean): Event {
return dispatchEvent(node, createFakeEvent(type, canBubble));
export function dispatchFakeEvent(node: Node | Window, type: string, bubbles?: boolean): Event {
return dispatchEvent(node, createFakeEvent(type, bubbles));
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/cdk/testing/testbed/fake-events/element-focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ function triggerFocusChange(element: HTMLElement, event: 'focus' | 'blur') {

/**
* Patches an elements focus and blur methods to emit events consistently and predictably.
* This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
* This is necessary, because some browsers can call the focus handlers asynchronously,
* while others won't fire them at all if the browser window is not focused.
* @docs-private
*/
// TODO: Check if this element focus patching is still needed for local testing,
// where browser is not necessarily focused.
export function patchElementFocus(element: HTMLElement) {
element.focus = () => dispatchFakeEvent(element, 'focus');
element.blur = () => dispatchFakeEvent(element, 'blur');
Expand Down
128 changes: 35 additions & 93 deletions src/cdk/testing/testbed/fake-events/event-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,36 @@ let uniqueIds = 0;
*/
export function createMouseEvent(
type: string, clientX = 0, clientY = 0, button = 0, modifiers: ModifierKeys = {}) {
const event = document.createEvent('MouseEvent');
const originalPreventDefault = event.preventDefault.bind(event);

// Note: We cannot determine the position of the mouse event based on the screen
// because the dimensions and position of the browser window are not available
// To provide reasonable `screenX` and `screenY` coordinates, we simply use the
// client coordinates as if the browser is opened in fullscreen.
const screenX = clientX;
const screenY = clientY;

event.initMouseEvent(type,
/* canBubble */ true,
/* cancelable */ true,
/* view */ window,
/* detail */ 0,
/* screenX */ screenX,
/* screenY */ screenY,
/* clientX */ clientX,
/* clientY */ clientY,
/* ctrlKey */ !!modifiers.control,
/* altKey */ !!modifiers.alt,
/* shiftKey */ !!modifiers.shift,
/* metaKey */ !!modifiers.meta,
/* button */ button,
/* relatedTarget */ null);
const event = new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
detail: 0,
relatedTarget: null,
screenX,
screenY,
clientX,
clientY,
ctrlKey: modifiers.control,
altKey: modifiers.alt,
shiftKey: modifiers.shift,
metaKey: modifiers.meta,
button: button,
buttons: 1,
});

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

// IE won't set `defaultPrevented` on synthetic events so we need to do it manually.
event.preventDefault = function() {
defineReadonlyEventProperty(event, 'defaultPrevented', true);
return originalPreventDefault();
};

return event;
}

Expand Down Expand Up @@ -85,10 +77,10 @@ export function createPointerEvent(type: string, clientX = 0, clientY = 0,
* @docs-private
*/
export function createTouchEvent(type: string, pageX = 0, pageY = 0, clientX = 0, clientY = 0) {
// In favor of creating events that work for most of the browsers, the event is created
// as a basic UI Event. The necessary details for the event will be set manually.
// We cannot use the `TouchEvent` or `Touch` because Firefox and Safari lack support.
// TODO: Switch to the constructor API when it is available for Firefox and Safari.
const event = document.createEvent('UIEvent');
const touchDetails = {pageX, pageY, clientX, clientY, id: uniqueIds++};
const touchDetails = {pageX, pageY, clientX, clientY, identifier: uniqueIds++};

// TS3.6 removes the initUIEvent method and suggests porting to "new UIEvent()".
(event as any).initUIEvent(type, true, true, window, 0);
Expand All @@ -108,75 +100,25 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 0, clientX = 0
*/
export function createKeyboardEvent(type: string, keyCode: number = 0, key: string = '',
modifiers: ModifierKeys = {}) {
const event = document.createEvent('KeyboardEvent');
const originalPreventDefault = event.preventDefault.bind(event);

// Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`.
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyEvent.
if ((event as any).initKeyEvent !== undefined) {
(event as any).initKeyEvent(type, true, true, window, modifiers.control, modifiers.alt,
modifiers.shift, modifiers.meta, keyCode);
} else {
// `initKeyboardEvent` expects to receive modifiers as a whitespace-delimited string
// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyboardEvent
let modifiersList = '';

if (modifiers.control) {
modifiersList += 'Control ';
}

if (modifiers.alt) {
modifiersList += 'Alt ';
}

if (modifiers.shift) {
modifiersList += 'Shift ';
}

if (modifiers.meta) {
modifiersList += 'Meta ';
}

// TS3.6 removed the `initKeyboardEvent` method and suggested porting to
// `new KeyboardEvent()` constructor. We cannot use that as we support IE11.
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyboardEvent.
(event as any).initKeyboardEvent(type,
true, /* canBubble */
true, /* cancelable */
window, /* view */
0, /* char */
key, /* key */
0, /* location */
modifiersList.trim(), /* modifiersList */
false /* repeat */);
}

// Webkit Browsers don't set the keyCode when calling the init function.
// See related bug https://bugs.webkit.org/show_bug.cgi?id=16735
defineReadonlyEventProperty(event, 'keyCode', keyCode);
defineReadonlyEventProperty(event, 'key', key);
defineReadonlyEventProperty(event, 'ctrlKey', !!modifiers.control);
defineReadonlyEventProperty(event, 'altKey', !!modifiers.alt);
defineReadonlyEventProperty(event, 'shiftKey', !!modifiers.shift);
defineReadonlyEventProperty(event, 'metaKey', !!modifiers.meta);

// IE won't set `defaultPrevented` on synthetic events so we need to do it manually.
event.preventDefault = function() {
defineReadonlyEventProperty(event, 'defaultPrevented', true);
return originalPreventDefault();
};

return event;
return new KeyboardEvent(type, {
bubbles: true,
cancelable: true,
view: window,
keyCode: keyCode,
key: key,
shiftKey: modifiers.shift,
metaKey: modifiers.meta,
altKey: modifiers.alt,
ctrlKey: modifiers.control,
});
}

/**
* Creates a fake event object with any desired event type.
* @docs-private
*/
export function createFakeEvent(type: string, canBubble = false, cancelable = true) {
const event = document.createEvent('Event');
event.initEvent(type, canBubble, cancelable);
return event;
export function createFakeEvent(type: string, bubbles = false, cancelable = true) {
return new Event(type, {bubbles, cancelable});
}

/**
Expand Down