Skip to content

Commit

Permalink
feat(#315): ngMocks.trigger and ngMocks.click
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Mar 29, 2021
1 parent 93d5813 commit 2ae6e5a
Show file tree
Hide file tree
Showing 19 changed files with 1,130 additions and 99 deletions.
152 changes: 77 additions & 75 deletions .github/workflows/ci.yaml

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/articles/api/ngMocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ access desired elements and instances in fixtures.
- [`change()`](ngMocks/change.md)
- [`touch()`](ngMocks/touch.md)

## Simulating HTML events

- [`click()`](ngMocks/click.md)
- [`trigger()`](ngMocks/trigger.md)
- [`event()`](ngMocks/event.md)

## Manipulating `ng-template`

- [`render()`](ngMocks/render.md)
Expand Down
31 changes: 31 additions & 0 deletions docs/articles/api/ngMocks/click.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: ngMocks.click
description: Documentation about ngMocks.click, a simple tool to click any element in unit tests
---

There are several ways how to click an element in Angular unit tests.
However, `.triggerEventHandler` does not respect `disabled` state and does not call a native `click` event.
And `.click` on a `nativeElement` does not allow customizing event properties.

`ngMocks.click` is a simple tool which covers these limitations:

- it respects disabled state
- it allows customizations of events
- it causes native events

```ts
const el = ngMocks.find('a');

// we can click debug elements
ngMocks.click(el);

// we can click native elements
// with custom coordinates
ngMocks.click(el.nativeElement, {
x: 150,
y: 150,
});
```

Under the hood `ngMocks.click` uses [`ngMocks.trigger`](./trigger.md),
therefore all features of [`ngMocks.trigger`](./trigger.md) can be used.
17 changes: 17 additions & 0 deletions docs/articles/api/ngMocks/event.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
title: ngMocks.event
description: Documentation about ngMocks.event, a simple interface to create custom events in unit tests
---

`ngMocks.event` solves the legacy of IE11, when an event object cannot be created via `new CustomEvent`, but via `document.createEvent`.

Besides that, `ngMocks.event` provides a simple interface to customize the event properties.

```ts
const event = ngMocks.event('click', {
x: 1,
y: 2,
});
```

The created event can be dispatched via [`ngMocks.trigger`](./trigger.md#custom-events).
42 changes: 42 additions & 0 deletions docs/articles/api/ngMocks/trigger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
title: ngMocks.trigger
description: Documentation about ngMocks.trigger, a tool with a simple interface to trigger any events in unit tests
---

`ngMocks.trigger` provides a simple interface which allows to trigger all the variety of events and to customize their properties.

## Common events

For example, a focus event can be triggered like that:

```ts
const el = ngMocks.find('input');
ngMocks.trigger(el, 'focus');
ngMocks.trigger(el, 'blur');
ngMocks.trigger(el, 'mouseleave', {
x: 1,
y: 2,
});
```

## Key combinations

In order to simulate shot keys and test their handlers,
for example, `Control+Shift+Z`:

```ts
const el = ngMocks.find('input');
ngMocks.trigger(el, 'keydown.control.shift.z');
ngMocks.trigger(el, 'keyup.meta.s');
```

## Custom events

Instead of the name of an event, an event object can be passed.
In order to create an event object [`ngMocks.event`](./event.md) can be used.

```ts
const el = ngMocks.find('input');
const event = new CustomEvent('my-event');
ngMocks.trigger(el, event);
```
3 changes: 3 additions & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ module.exports = {
'api/ngMocks/globalWipe',
'api/ngMocks/change',
'api/ngMocks/touch',
'api/ngMocks/click',
'api/ngMocks/trigger',
'api/ngMocks/event',
'api/ngMocks/render',
'api/ngMocks/hide',
'api/ngMocks/input',
Expand Down
27 changes: 11 additions & 16 deletions libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,25 @@ import { DebugElement } from '@angular/core';

import coreForm from '../../common/core.form';
import { isMockControlValueAccessor } from '../../common/func.is-mock-control-value-accessor';
import mockHelperTrigger from '../events/mock-helper.trigger';
import mockHelperStubMember from '../mock-helper.stub-member';

import funcGetVca from './func.get-vca';

// default html behavior
const triggerInput = (el: DebugElement, value: any): void => {
const target = el.nativeElement;
el.triggerEventHandler('focus', {
target,
});

const descriptor = Object.getOwnPropertyDescriptor(target, 'value');
mockHelperStubMember(target, 'value', value);
el.triggerEventHandler('input', {
target,
});
mockHelperTrigger(el, 'focus');

const descriptor = Object.getOwnPropertyDescriptor(el.nativeElement, 'value');
mockHelperStubMember(el.nativeElement, 'value', value);
mockHelperTrigger(el, 'input');
mockHelperTrigger(el, 'change');
if (descriptor) {
Object.defineProperty(target, 'value', descriptor);
target.value = value;
Object.defineProperty(el.nativeElement, 'value', descriptor);
el.nativeElement.value = value;
}

el.triggerEventHandler('blur', {
target,
});
mockHelperTrigger(el, 'blur');
};

const handleKnown = (valueAccessor: any, value: any): boolean => {
Expand All @@ -51,7 +46,7 @@ const handleKnown = (valueAccessor: any, value: any): boolean => {
};

const hasListener = (el: DebugElement): boolean =>
el.listeners.filter(listener => listener.name === 'input').length > 0;
el.listeners.filter(listener => listener.name === 'input' || listener.name === 'change').length > 0;

const keys = ['onChange', '_onChange', 'changeFn', '_onChangeCallback', 'onModelChange'];

Expand Down
11 changes: 3 additions & 8 deletions libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.touch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@ import { DebugElement } from '@angular/core';

import coreForm from '../../common/core.form';
import { isMockControlValueAccessor } from '../../common/func.is-mock-control-value-accessor';
import mockHelperTrigger from '../events/mock-helper.trigger';

import funcGetVca from './func.get-vca';

// default html behavior
const triggerTouch = (el: DebugElement): void => {
const target = el.nativeElement;
el.triggerEventHandler('focus', {
target,
});

el.triggerEventHandler('blur', {
target,
});
mockHelperTrigger(el, 'focus');
mockHelperTrigger(el, 'blur');
};

const handleKnown = (valueAccessor: any): boolean => {
Expand Down
5 changes: 5 additions & 0 deletions libs/ng-mocks/src/lib/mock-helper/events/mock-helper.click.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import mockHelperTrigger from './mock-helper.trigger';

export default (selector: any, payload?: object) => {
mockHelperTrigger(selector, 'click', payload);
};
179 changes: 179 additions & 0 deletions libs/ng-mocks/src/lib/mock-helper/events/mock-helper.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import mockHelperStub from '../mock-helper.stub';

/**
* @see https://developer.mozilla.org/de/docs/Web/Events
*/
const preventBubble = ['focus', 'blur', 'load', 'unload', 'change', 'reset', 'scroll'];

// istanbul ignore next
function customEvent(event: string, params?: EventInit) {
const initParams = {
bubbles: false,
cancelable: false,
...params,
};
const eventObj = document.createEvent('CustomEvent');
eventObj.initCustomEvent(event, initParams.bubbles, initParams.cancelable, null);

return eventObj;
}

const eventCtor =
typeof (Event as any) === 'function'
? (event: string, init?: EventInit): CustomEvent => new CustomEvent(event, init)
: /* istanbul ignore next */ customEvent;

const keyMap: Record<string, object> = {
alt: {
altKey: true,
code: 'AltLeft',
key: 'Alt',
location: 1,
which: 18,
},
arrowdown: {
code: 'ArrowDown',
key: 'ArrowDown',
location: 0,
which: 40,
},
arrowleft: {
code: 'ArrowLeft',
key: 'ArrowLeft',
location: 0,
which: 37,
},
arrowright: {
code: 'ArrowRight',
key: 'ArrowRight',
location: 0,
which: 39,
},
arrowup: {
code: 'ArrowUp',
key: 'ArrowUp',
location: 0,
which: 38,
},
backspace: {
code: 'Backspace',
key: 'Backspace',
location: 0,
which: 8,
},
control: {
code: 'ControlLeft',
ctrlKey: true,
key: 'Control',
location: 1,
which: 17,
},
enter: {
code: 'Enter',
key: 'Enter',
location: 0,
which: 13,
},
esc: {
code: 'Escape',
key: 'Escape',
location: 0,
which: 27,
},
meta: {
code: 'MetaLeft',
key: 'Meta',
location: 1,
metaKey: true,
which: 91,
},
shift: {
code: 'ShiftLeft',
key: 'Shift',
location: 1,
shiftKey: true,
which: 16,
},
space: {
code: 'Space',
key: ' ',
location: 0,
which: 32,
},
tab: {
code: 'Tab',
key: 'Tab',
location: 0,
which: 9,
},
};
for (let f = 1; f <= 12; f += 1) {
keyMap[`f${f}`] = {
code: `F${f}`,
key: `F${f}`,
location: 0,
which: f + 111,
};
}

const getCode = (char: string): string => {
const code = char.codePointAt(0);
// a-z
if (code && code >= 97 && code <= 122) {
return `Key${char.toUpperCase()}`;
}
// A-Z
if (code && code >= 65 && code <= 90) {
return `Key${char.toUpperCase()}`;
}
// A-Z
if (code && code >= 48 && code <= 57) {
return `Digit${char}`;
}

return 'Unknown';
};

const applyPayload = (event: Event, payload?: string): void => {
const keyData: object = {};
for (const key of payload ? payload.split('.') : []) {
let map = keyMap[key];
if (!map && key.length === 1) {
map = {
code: getCode(key),
key,
};
}

if (!map) {
throw new Error(`Unknown event part ${key}`);
}

mockHelperStub(keyData, map);
}

if (payload) {
mockHelperStub(event, keyData);
}
};

export default (
event: string,
init?: EventInit,
overrides?: Partial<UIEvent | KeyboardEvent | MouseEvent | TouchEvent | Event>,
): CustomEvent => {
const dot = event.indexOf('.');
const [eventName, eventPayload] = dot === -1 ? [event] : [event.substr(0, dot), event.substr(dot + 1)];
const eventObj = eventCtor(eventName, {
bubbles: preventBubble.indexOf(event) === -1,
cancelable: true,
...init,
});
applyPayload(eventObj, eventPayload);

if (overrides) {
mockHelperStub(eventObj, overrides);
}

return eventObj;
};

0 comments on commit 2ae6e5a

Please sign in to comment.