Skip to content
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

element.focus() not setting document.activeElement #2337

Closed
2 of 13 tasks
vire opened this issue Feb 14, 2020 · 15 comments
Closed
2 of 13 tasks

element.focus() not setting document.activeElement #2337

vire opened this issue Feb 14, 2020 · 15 comments
Labels

Comments

@vire
Copy link

vire commented Feb 14, 2020

Current behavior

After explicitly calling .focus() on DOM Node under test document.activeElement switches from <body> to null

image

Expected behavior

After explicitly calling .focus() on DOM Node under testdocument.activeElement switches to DOM Node under test

Your environment

image

NOTE: this was working with testEnvironment: 'jest-environment-jsdom-fourteen',

API

  • shallow
  • mount
  • render

Version

library version
jest 25.1
enzyme 3.11.0
react 16.12
react-dom 16.12
jsdom 15.2
adapter (below)

Adapter

  • enzyme-adapter-react-16
  • enzyme-adapter-react-16.3
  • enzyme-adapter-react-16.2
  • enzyme-adapter-react-16.1
  • enzyme-adapter-react-15
  • enzyme-adapter-react-15.4
  • enzyme-adapter-react-14
  • enzyme-adapter-react-13
  • enzyme-adapter-react-helper
  • others ( )
@ljharb
Copy link
Member

ljharb commented Feb 14, 2020

JSDOM is in full control of how .focus() works; enzyme (or jest) has nothing to do with it.

The implication is that this changed in JSDOM v15; I'd suggest investigating their changelog.

@ljharb ljharb closed this as completed Feb 14, 2020
@ljharb ljharb added the invalid label Feb 14, 2020
@vire
Copy link
Author

vire commented Feb 15, 2020

@ljharb thanks for the response, I think you're right but enzyme API plays a role in this issue as well.
to fix a failing test document.activeElement === inputDOMNode thx to ant-design/ant-design#21185

is to add { attachTo: document.body }

  const wrapper = mount(<input type="text" />, { attachTo: document.body });

@ljharb
Copy link
Member

ljharb commented Feb 15, 2020

That enzyme can be used to work around a jsdom issue doesn’t change the source of the issue :-)

@pbadenski
Copy link

thank you for this! just had the very same issue.

@BPScott
Copy link

BPScott commented Apr 4, 2020

Thread necromancy but I thiiink this might be an issue in enzyme after all.

That enzyme can be used to work around a jsdom issue doesn’t change the source of the issue :-)

Doing a bit of investigation, I think jsdom has gotten stricter and more in-line with what browsers do between v14 and v16 that Jest v25 uses. I think jsdom is in the right here - it matches browser behaviour, and that enzyme will need to mount elements slightly differently if it wants to have document.activeElement be set.

Consider the following JSBin: https://jsbin.com/tuqonikavu/edit?js,console,output

It shows that in a browser:

  • document.activeElement DOES get set if the element you call .focus() on is attached to the body element (present on the page? not sure of the right term for this)
  • document.activeElement DOES NOT get set if the element you call is created but not attached to the body

Looking at the react adaptor it looks like the element that you create to mount an element onto by default is global.document.createElement('div'); which isn't attached to the body element.

That sounds like the current enzyme behaviour using jsdom v16 given the "default" behaviour and @vire's workaround using attachTo matches the behaviour of browsers.

To get this working out-the-box I think Enzyme will want to mount attach content somewhere under the document.body instead of on an unattached div.

@ljharb
Copy link
Member

ljharb commented Apr 4, 2020

@BPScott mount has an attachTo option, which you can provide any element you like - if that solves the issue.

@BPScott
Copy link

BPScott commented Apr 4, 2020

Yep that'll solve the issue in userland. However I get the feeling that this is going to trip a lot of folks up.

Personally, the team I'm on has written several tests that help ensure accessibility - that focus is moved to a particular element at a given element at a point in time, and I did not expect these to require modifications after a jsdom update. I expected that when I mounted elements that they'd be mounted within the body of a document and thus focus changing would work like when I have visible elements within a browser - do my manual checking with elements attached to a body I would have thought my automated tests would work in the same way by default.

I think we want to encourage people to write tests around focus management, and making it so they have to add attachTo and then clean up after themselves will disincentive people from doing that. I wonder if it possible to change Enzyme's default behaviour (i.e. it should attach elements onto the body) to make writing these tests as lean as possible.

Low-effort userland solution:

This is subtly buggy - if the expect check fails then then ele is never unmounted and we won't be working off a clean slate for future tests, which may lead to cascading failures in unrelated tests which will be hard to debug. This is a footgun waiting to happen IMO.

it('should focus', () => {
  const ele = mount(<button>Hi</button>, { attachTo: document.body });
  ele.focus();
  expect(document.activeElement).toBe(ele.getDOMNode());
  // Clean up
  ele.unmount();
});

Bulletproof userland solution:

This fixes the "expect failures will cause leakages into other tests" problem but it is a lot more boilerplate. I'd wager most people won't be aware of this subtlety and go for the lower-effort solution mentioned above

describe('my thing', () => {
  let ele;

  afterAll(() => {
    ele && ele.unmount();
  })

  it('should focus', () => {
    ele = mount(<button>Hi</button>, { attachTo: document.body });
    ele.focus();
    expect(document.activeElement).toBe(ele.getDOMNode());
  });
})

Ideal end state, this currently does not work because mount does not attach things to the body by default:

it('should focus', () => {
  const ele = mount(<button>Hi</button>);
  ele.focus();
  expect(document.activeElement).toBe(ele.getDOMNode());
});

@ljharb
Copy link
Member

ljharb commented Apr 4, 2020

ReactDOM.render forces you to supply an element to attach to - I'm not sure why people would expect mount to implicitly guess which HTML element is safe to obliterate.

I'm happy to pursue a better solution - I'm not sure what it would be, though (note, mount wrappers do not have a focus method, only the elements themselves do)

@BPScott
Copy link

BPScott commented Apr 9, 2020

note, mount wrappers do not have a focus method, only the elements themselves do)

Ah yes, sorry - have updated those references to ele to be ele.getDOMNode()

@jtomaszewski
Copy link

Enzyme could export a utility function that we would use like:

import { buildEnzymeWrapper } from 'enzyme/jest';

describe('', () => {
  const { mount }  = buildEnzymeWrapper();
  
  it('should focus', () => {
    ele = mount(<button>Hi</button>);
    ele.focus();
    expect(document.activeElement).toBe(ele.getDOMNode());
  });
});

The buildEnzymeWrapper function would use beforeEach and afterEach jest functions under the hood in order to create/remove div wrapper that is attached to document.body whenever needed.

Then the devs wouldn't be required to clean up the wrappers, nor remember about the attachTo: document.body thing. Of course we could make it configurable a bit.

@jtomaszewski
Copy link

jtomaszewski commented Apr 16, 2020

For now, I've build myself a simple function like this:

/**
 * If you want to have your element to be attached to `document.body`
 * (which is needed for example when you want to test focusing by referencing `document.activeElement`),
 * use this to build a container element
 * that can be later passed to the `mount()` function as `attachTo` param.
 *
 * Usage:
 * ```tsx
 * describe('Button', () => {
 *   const attachTo = buildMountAttachTarget();
 *
 *   it('works', () => {
 *     const output = mount(<Button />, {
 *       attachTo,
 *     });
 *   });
 * });
 * ```
 */
export function buildMountAttachTarget() {
  let container: HTMLDivElement;

  beforeEach(() => {
    container = document.createElement('div');
    document.body.append(container);
  });

  afterEach(() => {
    document.body.removeChild(container!);
  });

  return container!;
}

However it would be very nice to have that built-in in the Enzyme library. (Honestly, I think I wrote something similar to it for the 3rd time already, each time in a different project.)

@ljharb
Copy link
Member

ljharb commented Apr 17, 2020

@jtomaszewski enzyme couldn't export that because then it'd be tightly coupled to a single test runner. All our docs are written in mocha + chai, and you can use enzyme with any test runner you like, or none at all - if enzyme required jest, then a lot of stuff would be easier, but not everyone uses jest.

@jtomaszewski
Copy link

jtomaszewski commented Apr 17, 2020

Yeah, I know this. But I'm sure we could have a separate package that everybody using both enzyme and jest could use. (Nowadays it seems like 90% of community is using jest; ignoring that would be ignoring that community.)

Should I file such feature requests against other repository (or create my own)?
Or should I file it here?

I mean, we could have such helpers living at https://github.com/enzymejs/enzyme/tree/master/packages/enzyme-jest-helpers , for example. I could help developing that a bit.

@ljharb
Copy link
Member

ljharb commented Apr 17, 2020

That’s be a fine project to explore.

@vladanpaunovic
Copy link

vladanpaunovic commented Apr 17, 2020

In case it can help someone, this is what I did in our project:

In Jest setup file:

Note: In case you are testing NodeJS with the same Jest setup, you have to divide it by client and server and add this code only in the client as on server you won't have access to document and it will be throwing errors.

// setupJest.client.js

/**
 * In order to attach a mounted component to a div element we have to create
 * and remove an element from the node before and after each test.
 */
let container: HTMLDivElement | null;

beforeEach(() => {
  container = document.createElement("div");
  container.id = "enzymeContainer";
  document.body.appendChild(container);
});

afterEach(() => {
  if (container && container.parentNode) {
    container.parentNode.removeChild(container);
  }

  container = null;
});

Then I created a custom wrapper for mounting, using Enzyme mount, like so:

// jest/containers.ts

import { mount as enzymeMount } from "enzyme";

export const mount = (component: ReactNode) =>
  enzymeMount(component, {
    attachTo: document.getElementById("enzymeContainer")
  });

Usage:

// exampleTest.ts

import { mount } from "./jest/containers.ts";

describe('', () => {
  it('should focus', () => {
    ele = mount(<button>Hi</button>);
    ele.focus();
    expect(document.activeElement).toBe(ele.getDOMNode());
  });
});

This approach works very well, and we are using it with around 4k of tests, without any issues. The only drawback with this is that you can have only one mounting per test, so be aware of it.

I am happy to provide more details should you need it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants