Skip to content

Commit 9ff8bd4

Browse files
committed
fix(focus): don't use focus patch in jest (ngneat#373)
In Jest, don't patch HTML Element focus() or blur() methods, b/c they break correct jsdom behavior. In browsers, improve HTML Element focus() patch so it triggers a blur on the activeElement, and updates the activeElement.
1 parent 08e5bae commit 9ff8bd4

File tree

4 files changed

+122
-2
lines changed

4 files changed

+122
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
});
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { dispatchFakeEvent } from '../dispatch-events';
2+
import { isRunningInJsDom } from '../utils';
3+
4+
/** Property name added to HTML Elements to ensure we don't double-patch focus methods on an element. */
5+
const IS_FOCUS_PATCHED_PROP = '_patched_focus';
26

37
/**
48
* Patches an elements focus and blur methods to emit events consistently and predictably.
@@ -8,6 +12,22 @@ import { dispatchFakeEvent } from '../dispatch-events';
812
* patchElementFocus(triggerEl);
913
*/
1014
export function patchElementFocus(element: HTMLElement): void {
11-
element.focus = () => dispatchFakeEvent(element, 'focus');
12-
element.blur = () => dispatchFakeEvent(element, 'blur');
15+
16+
// https://github.com/ngneat/spectator/issues/373 - Don't patch when using JSDOM, eg in Jest
17+
if (!isRunningInJsDom() && (element[IS_FOCUS_PATCHED_PROP] === undefined)) {
18+
const baseFocus = element.focus.bind(element);
19+
element.focus = (options) => {
20+
// Blur current active
21+
const active = element.ownerDocument.activeElement;
22+
if (active) {
23+
dispatchFakeEvent(active, 'blur');
24+
}
25+
26+
// Focus
27+
baseFocus(options); // Needed to set document.activeElement
28+
dispatchFakeEvent(element, 'focus'); // Needed to cause focus event
29+
}
30+
element.blur = () => dispatchFakeEvent(element, 'blur');
31+
element[IS_FOCUS_PATCHED_PROP] = true;
32+
}
1333
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
});
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)