Skip to content
New issue

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

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

Already on GitHub? # to your account

Bug/373 jest focus #473

Merged
merged 3 commits into from
Aug 10, 2021
Merged

Bug/373 jest focus #473

merged 3 commits into from
Aug 10, 2021

Conversation

johncrim
Copy link
Contributor

@johncrim johncrim commented Aug 8, 2021

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

[x] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] Other... Please describe:

What is the current behavior?

Issue Number: #373

What is the new behavior?

  1. In Jest (more specifically jsdom), the patch for HTMLElement.focus() and HTMLElement.blur() is not applied.
  2. In browsers, when the HTMLElement.focus() is patched the patched implementation is more robust. It calls the browser's HTMLElement.focus() method first, and if the focus and/or blur events are not detected when focus() is called, it sends the fake events. This ensures that document.activeElement is set (so the toBeFocused() matcher works, and app behavior that depends on document.activeElement works), and it ensures that a single focus event, and 0-1 blur events, are emitted.

Does this PR introduce a breaking change?

[ ] Yes
[x] No

There is no API change, but there is a change in behavior, that could conceivably break tests that depend on the old behavior (most likely to work around it). However this should be unlikely.

focus() and blur() were functionally broken in Jest, so this shouldn't break jest tests. The change for jest is that the jsdom focus() and blur() methods are no longer patched, but they didn't need to be.

With the previous implementation, focus() and blur() worked partially in karma, but document.activeElement wasn't set by the patched methods. The old behavior ensured that focus and blur events were always sent when their respective methods were called, but it didn't handle blurring the previous activeElement. It also didn't set the activeElement. The new implementation calls the browser's focus() method, which seems to always set the document.activeElement, but doesn't always send focus/blur events depending on the browser state. The new patch listens for browser-emitted focus/blur events, and sends fake ones (same as before) if they weren't emitted by the browser.

It might be a good idea to call the browser's blur() method first, and send a blur event only if one doesn't occur. However I haven't had to deal with this possible inconsistency between test environments and browsers, so I didn't make this fix.

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.
In browsers, HTMLElement.focus() always sets `document.activeElement`, but
focus + blur events may or may not be sent depending on focus. This ensures
that focus + blur events are always sent if appropriate, and `document.activeElement` is set.
@johncrim
Copy link
Contributor Author

johncrim commented Aug 8, 2021

Let me know if the docs need to be updated - I'm not aware of a need.

this._active?.addEventListener('blur', this);
}
public handleEvent(evt: Event): void {
if (evt.type === 'focus') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this._focused = type === 'focus';
this._blurred = type === 'blur';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's incorrect - we are listening for both a blur event and a focus event on different elements, that would overwrite the first event when the second event occurs.

private _focused = false;

constructor(private readonly _e: HTMLElement) {
this._active = _e.ownerDocument.activeElement;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this._active = _e.ownerDocument.activeElement;
this.activeElement = _e.ownerDocument.activeElement;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with priorActiveElement.

this._active?.removeEventListener('blur', this);

// Ensure activeElement is blurred
if (this._active && !this._blurred && this._active === this._e.ownerDocument.activeElement) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the following condition more accurate?

if(!this._blurred && this.activeElement === this.element) {}

Copy link
Contributor Author

@johncrim johncrim Aug 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activeElement should be the previous document.activeElement when the watcher was created - I'm renaming the field to clarify that.

But I think you're right that the ownerDocument.activeElement check is wrong and potentially problematic (interesting that it wasn't caught by tests). We do expect the document.activeElement to change when the original HTMLElement.focus() method is called. Since the focus() method isn't cancellable in event handlers, I think the blur event should be sent, if it wasn't sent by the browser, regardless of how the document.activeElement has changed from calling HTMLElement.focus().

* This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
* while others won't fire them at all if the browser window is not focused.
*
* patchElementFocus(triggerEl);
*/
export function patchElementFocus(element: HTMLElement): void {
element.focus = () => dispatchFakeEvent(element, 'focus');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why deleting the Jaminse functionality?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's replaced with a better patched focus().

Also, fix potentially problematic blur check.
@johncrim
Copy link
Contributor Author

johncrim commented Aug 9, 2021

I've addressed all your feedback.

@NetanelBasal NetanelBasal merged commit f7b2f64 into ngneat:master Aug 10, 2021
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants