'How to test for document being undefined with RTL?

I have the following react hook which brings focus to a given ref and on unmount returns the focus to the previously focused element.


export default function useFocusOnElement(elementRef: React.RefObject<HTMLHeadingElement>) {
  const documentExists = typeof document !== 'undefined';
  const [previouslyFocusedEl] = useState(documentExists && (document.activeElement as HTMLElement));

  useEffect(() => {
    if (documentExists) {
      elementRef.current?.focus();
    }

    return () => {
      if (previouslyFocusedEl) {
        previouslyFocusedEl?.focus();
      }
    };
  }, []);
}

Here is the test I wrote for it.

/**
 * @jest-environment jsdom
 */

describe('useFocusOnElement', () => {
  let ref: React.RefObject<HTMLDivElement>;
  let focusMock: jest.SpyInstance;

  beforeEach(() => {
    ref = { current: document.createElement('div') } as React.RefObject<HTMLDivElement>;
    focusMock = jest.spyOn(ref.current as HTMLDivElement, 'focus');
  });

  it('will call focus on passed ref after mount ', () => {
    expect(focusMock).not.toHaveBeenCalled();
    renderHook(() => useFocusOnElement(ref));
    expect(focusMock).toHaveBeenCalled();
  });
});

I would like to also test for the case where document is undefined as we also do SSR. In the hook I am checking for the existence of document and I would like to test for both cases.

JSDOM included document so I feel I'd need to remove it and some how catch an error in my test?



Solution 1:[1]

I ended up using wrapWithGlobal and wrapWithOverride from https://github.com/airbnb/jest-wrap.


describe('useFocusOnElement', () => {
  let ref: React.RefObject<HTMLDivElement>;
  let focusMock: jest.SpyInstance;
  let activeElMock: unknown;
  let activeEl: HTMLDivElement;

  beforeEach(() => {
    const { window } = new JSDOM();
    global.document = window.document;
    activeEl = document.createElement('div');
    ref = { current: document.createElement('div') };
    focusMock = jest.spyOn(ref.current as HTMLDivElement, 'focus');
    activeElMock = jest.spyOn(activeEl, 'focus');
  });
  wrapWithOverride(
    () => document,
    'activeElement',
    () => activeEl,
  );
  describe('when document present', () => {
    it('will focus on passed ref after mount and will focus on previously active element on unmount', () => {
      const hook = renderHook(() => useFocusOnElement(ref));
      expect(focusMock).toHaveBeenCalled();
      hook.unmount();
      expect(activeElMock).toHaveBeenCalled();
    });
  });

  describe('when no document present', () => {
    wrapWithGlobal('document', () => undefined);

    it('will not call focus on passed ref after mount nor on previously active element on unmount', () => {
      const hook = renderHook(() => useFocusOnElement(ref));
      expect(focusMock).not.toHaveBeenCalled();
      hook.unmount();
      expect(activeElMock).not.toHaveBeenCalled();
    });
  });
});

Solution 2:[2]

First of all, to simulate document as undefined, you should mock it like:

jest
   .spyOn(global as any, 'document', 'get')
   .mockImplementationOnce(() => undefined);

But to this work in your test, you will need to set spyOn inside renderHook because looks like it also makes use of document internally, and if you set spyOn before it, you will get an error.

Working test example:

it('will NOT call focus on passed ref after mount', () => {
    expect(focusMock).not.toHaveBeenCalled();

    renderHook(() => {
      jest
        .spyOn(global as any, 'document', 'get')
        .mockImplementationOnce(() => undefined);

      useFocusOnElement(ref);
    });

    expect(focusMock).not.toHaveBeenCalled();
});

Solution 3:[3]

You should be able to do this by creating a second test file with a node environment:

/**
 * @jest-environment node
 */

describe('useFocusOnElement server-side', () => {
  ...
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 criver.to
Solution 2 Luis Paulo Pinto
Solution 3 helloitsjoe