Skip to content

Commit f7b2f64

Browse files
authored
Bug/373 jest focus (#473)
1 parent 08e5bae commit f7b2f64

File tree

4 files changed

+184
-3
lines changed

4 files changed

+184
-3
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { SpectatorHost, createHostFactory } from '@ngneat/spectator/jest';
2+
3+
import { TestFocusComponent } from '../../../test/focus/test-focus.component';
4+
5+
describe('SpectatorHost.focus() in jest', () => {
6+
7+
const createHost = createHostFactory(TestFocusComponent);
8+
let host: SpectatorHost<TestFocusComponent>;
9+
10+
beforeEach(() => {
11+
host = createHost('<app-test-focus></app-test-focus>');
12+
})
13+
14+
it('sets document.activeElement', () => {
15+
host.focus('#button1');
16+
17+
expect(host.query('#button1')).toBeFocused();
18+
});
19+
20+
it('causes blur events', () => {
21+
host.focus();
22+
host.focus('#button1');
23+
host.focus('#button2');
24+
25+
expect(host.component.focusCount('app-test-focus')).toBe(1);
26+
expect(host.component.blurCount('app-test-focus')).toBe(1);
27+
expect(host.component.focusCount('button1')).toBe(1);
28+
expect(host.component.blurCount('button1')).toBe(1);
29+
expect(host.component.focusCount('button2')).toBe(1);
30+
expect(host.component.blurCount('button2')).toBe(0);
31+
});
32+
33+
34+
it('calling focus() multiple times does not cause multiple patches', () => {
35+
host.focus('#button1');
36+
host.focus();
37+
host.focus('#button1');
38+
39+
expect(host.component.focusCount('app-test-focus')).toBe(1);
40+
expect(host.component.focusCount('button1')).toBe(2);
41+
expect(host.component.blurCount('button1')).toBe(1);
42+
});
43+
44+
});
Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,73 @@
11
import { dispatchFakeEvent } from '../dispatch-events';
2+
import { isRunningInJsDom } from '../utils';
3+
4+
/** Property added to HTML Elements to ensure we don't double-patch focus methods on an element. */
5+
const IS_FOCUS_PATCHED_PROP = Symbol('isFocusPatched');
6+
7+
/** Ensures that a single set of matching focus and blur events occur when HTMLElement.focus() is called. */
8+
class FocusEventWatcher implements EventListenerObject {
9+
10+
private readonly priorActiveElement: Element | null;
11+
12+
/** Set to true when browser sends a blur event for priorActiveElement */
13+
private _blurred = false;
14+
/** Set to true when browser sends a focus event for element */
15+
private _focused = false;
16+
17+
constructor(private readonly element: HTMLElement) {
18+
this.element.addEventListener('focus', this);
19+
this.priorActiveElement = element.ownerDocument.activeElement;
20+
this.priorActiveElement?.addEventListener('blur', this);
21+
}
22+
23+
public handleEvent({ type }: Event): void {
24+
if (type === 'focus') {
25+
this._focused = true;
26+
}
27+
else if (type === 'blur') {
28+
this._blurred = true;
29+
}
30+
}
31+
32+
/**
33+
* If focus and blur events haven't occurred, fire fake ones.
34+
*/
35+
public ensureFocusEvents() {
36+
this.element.removeEventListener('focus', this);
37+
this.priorActiveElement?.removeEventListener('blur', this);
38+
39+
// Ensure priorActiveElement is blurred
40+
if (!this._blurred && this.priorActiveElement) {
41+
dispatchFakeEvent(this.priorActiveElement, 'blur');
42+
}
43+
44+
if (!this._focused) {
45+
dispatchFakeEvent(this.element, 'focus'); // Needed to cause focus event
46+
}
47+
}
48+
}
249

350
/**
4-
* Patches an elements focus and blur methods to emit events consistently and predictably.
51+
* Patches an element's focus and blur methods to emit events consistently and predictably in tests.
552
* This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
653
* while others won't fire them at all if the browser window is not focused.
754
*
855
* patchElementFocus(triggerEl);
956
*/
1057
export function patchElementFocus(element: HTMLElement): void {
11-
element.focus = () => dispatchFakeEvent(element, 'focus');
12-
element.blur = () => dispatchFakeEvent(element, 'blur');
58+
59+
// https://github.com/ngneat/spectator/issues/373 - Don't patch when using JSDOM, eg in Jest
60+
if (!isRunningInJsDom() && (element[IS_FOCUS_PATCHED_PROP] === undefined)) {
61+
const originalFocus = element.focus.bind(element);
62+
element.focus = (options) => {
63+
const focusEventWatcher = new FocusEventWatcher(element);
64+
65+
// Sets document.activeElement. May or may not send focus + blur events
66+
originalFocus(options);
67+
68+
focusEventWatcher.ensureFocusEvents();
69+
}
70+
element.blur = () => dispatchFakeEvent(element, 'blur');
71+
element[IS_FOCUS_PATCHED_PROP] = true;
72+
}
1373
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { SpectatorHost, createHostFactory } from '@ngneat/spectator';
2+
import { TestFocusComponent } from './test-focus.component';
3+
4+
describe('SpectatorHost.focus() ', () => {
5+
6+
const createHost = createHostFactory(TestFocusComponent);
7+
let host: SpectatorHost<TestFocusComponent>;
8+
9+
beforeEach(() => {
10+
host = createHost('<app-test-focus></app-test-focus>');
11+
})
12+
13+
it('sets document.activeElement', () => {
14+
host.focus('#button1');
15+
16+
expect(host.query('#button1')).toBeFocused();
17+
});
18+
19+
it('causes blur events', () => {
20+
host.focus();
21+
host.focus('#button1');
22+
host.focus('#button2');
23+
24+
expect(host.component.focusCount('app-test-focus')).toBe(1);
25+
expect(host.component.blurCount('app-test-focus')).toBe(1);
26+
expect(host.component.focusCount('button1')).toBe(1);
27+
expect(host.component.blurCount('button1')).toBe(1);
28+
expect(host.component.focusCount('button2')).toBe(1);
29+
expect(host.component.blurCount('button2')).toBe(0);
30+
});
31+
32+
it('calling focus() multiple times does not cause multiple patches', () => {
33+
host.focus('#button1');
34+
host.focus();
35+
host.focus('#button1');
36+
37+
expect(host.component.focusCount('app-test-focus')).toBe(1);
38+
expect(host.component.focusCount('button1')).toBe(2);
39+
expect(host.component.blurCount('button1')).toBe(1);
40+
});
41+
42+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-test-focus',
5+
template: `<button id="button1" (focus)="countFocus('button1')" (blur)="countBlur('button1')">Button1</button>
6+
<button id="button2" (focus)="countFocus('button2')" (blur)="countBlur('button2')">Button2</button>`,
7+
changeDetection: ChangeDetectionStrategy.OnPush,
8+
host: {
9+
'[attr.tabindex]': '0',
10+
'(focus)': 'countFocus("app-test-focus")',
11+
'(blur)': 'countBlur("app-test-focus")'
12+
}
13+
})
14+
export class TestFocusComponent {
15+
16+
private readonly focusCounts = new Map<string, number>();
17+
private readonly blurCounts = new Map<string, number>();
18+
19+
public countFocus(id: string) {
20+
this.focusCounts.set(id, this.focusCount(id) + 1);
21+
}
22+
23+
public countBlur(id: string) {
24+
this.blurCounts.set(id, this.blurCount(id) + 1);
25+
}
26+
27+
public focusCount(id: string): number {
28+
return this.focusCounts.get(id) ?? 0;
29+
}
30+
31+
public blurCount(id: string): number {
32+
return this.blurCounts.get(id) ?? 0;
33+
}
34+
35+
}

0 commit comments

Comments
 (0)