如何使用 RTL 测试未定义的文档?

How to test for document being undefined with RTL?

我有以下 React 挂钩,它将焦点带到给定的 ref 并卸载 returns 焦点到先前聚焦的元素。


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();
      }
    };
  }, []);
}

这是我为它写的测试。

/**
 * @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();
  });
});

我还想测试 document 未定义的情况,因为我们也做 SSR。在挂钩中,我正在检查 document 是否存在,我想测试这两种情况。

JSDOM 包含文档,所以我觉得我需要删除它以及一些如何在我的测试中捕获错误?

首先,要将 document 模拟为 undefined,您应该像这样模拟它:

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

但是对于你测试中的这项工作,你需要在 renderHook 中设置 spyOn 因为看起来它也在内部使用文档,如果你之前设置 spyOn它,你会得到一个错误。

工作测试示例:

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();
});

您应该可以通过使用节点环境创建第二个测试文件来做到这一点:

/**
 * @jest-environment node
 */

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

我最终使用了 https://github.com/airbnb/jest-wrap 中的 wrapWithGlobalwrapWithOverride


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();
    });
  });
});