在玩笑和打字稿中模拟路口观察者

Mock for intersection observer in jest and typescript

如果我们使用 jest 和 typescript,其中使用了交集观察器,交集观察器的模拟将变得困难。到目前为止我在:


beforeEach(() => {
  // IntersectionObserver isn't available in test environment
  const mockIntersectionObserver = class {
    observe() {
      console.log(this);
    }

    unobserve() {
      console.log(this);
    }

    disconnect() {
      console.log(this);
    }

    root = null

    rootMargin = '0'

    thresholds=[1]

    takeRecords=() => ([{
      isIntersecting: true,
      boundingClientRect: true,
      intersectionRatio: true,
      intersectionRect: true,
      rootBounds: true,
      target: true,
       time: true,
    }])
  };
  window.IntersectionObserver = mockIntersectionObserver;
});

但这仍然会抛出如下错误:

Type 'typeof mockIntersectionObserver' is not assignable to type '{ new (callback: IntersectionObserverCallback, options?: IntersectionObserverInit | undefined): IntersectionObserver; prototype: IntersectionObserver; }'.
  The types returned by 'prototype.takeRecords()' are incompatible between these types.
    Type '{ isIntersecting: boolean; boundingClientRect: boolean; intersectionRatio: boolean; intersectionRect: boolean; rootBounds: boolean; target: boolean; time: boolean; }[]' is not assignable to type 'IntersectionObserverEntry[]'.
      Type '{ isIntersecting: boolean; boundingClientRect: boolean; intersectionRatio: boolean; intersectionRect: boolean; rootBounds: boolean; target: boolean; time: boolean; }' is not assignable to type 'IntersectionObserverEntry'.
        Types of property 'boundingClientRect' are incompatible.
          Type 'boolean' is not assignable to type 'DOMRectReadOnly'.ts(2322

我可以不断地为每个元素添加正确的类型,但是有更好的方法吗?

如何将路口观察器添加到 jest 环境中?我觉得这样比嘲讽要好

一般来说,mock 应该严格遵循 IntersectionObserver and IntersectionObserverEntry 规范,但 mock 可以根据用途进行剥离。

如果正确的键入会使事情变得更加复杂,则可能没有必要在模拟中保持类型安全。在这种情况下,类型错误显示模拟中的错误。正如在 IntersectionObserverEntry 参考中看到的那样,只有 isIntersecting 属性 应该是一个布尔值,而 boundingClientRect 应该是一个对象,因此为其余部分提供布尔值并忽略类型问题可能会导致无意中不起作用的模拟实现。

使用常规 class 进行模拟是不切实际的,因为它缺少 Jest 间谍提供的功能,例如调用断言和由框架控制的模拟实现。

一个简单的实现是:

window.IntersectionObserver = jest.fn(() => ({
  takeRecords: jest.fn(),
  ...
}));

缺点是当无法直接访问实例或需要在实例化后立即完成时,无法更改模拟 class 成员的实现。这需要在需要时替换整个 class 实现。

出于这个原因,让 Jest spy class 具有可以在实例化之前访问的原型链是有益的。 Jest auto-mocking 可以被用于此目的,这允许为 read-only 属性定义 get 访问器,可以像任何其他 Jest 间谍一样更改实现:

class IntersectionObserverStub {
  get root() {} // read-only property 
  takeRecords() { /* implementation is ignored */ } // stub method
  observe() {}
  ...
}

jest.doMock('intersection-observer-mock', () => IntersectionObserverStub, { virtual: true });

window.IntersectionObserver = jest.requireMock('intersection-observer-mock');

jest.spyOn(IntersectionObserver.prototype, 'root', 'get').mockReturnValue(null);
// jest.spyOn(IntersectionObserver.prototype, 'takeRecords').mockImplementation(() => ({...}));

这导致生成模拟的 class 实现,它是 Jest 间谍原型,其方法是 no-op 间谍。 get 访问器保持原样,但由于它们存在,以后可以使用 spyOn(..., 'get') 模拟它们。像 takeRecords 这样可能特定于实例的方法可以在没有默认实现的情况下保留并被模拟 in-place,它 returns undefined 可能会导致比随机预定义更清晰的错误输出意外调用时的值。

jest.spyOn(IntersectionObserver.prototype, 'root', 'get').mockReturnValueOnce(someDocument);
const mockedEntries = [{
  isIntersecting: true,
  boundingClientRect: { x: 10, y: 20, width: 30, height: 40, ... },
  ...
}];
IntersectionObserver.prototype.takeRecords.mockReturnValueOnce(mockedEntries );

// code that instantiates IntersectionObserver and possibly uses mocked values immediately

expect(IntersectionObserver.prototype.observe).toBeCalledWith(...);
expect(IntersectionObserver).toBeCalledWith(expect.any(Function)); // callback arg
let [callback] = IntersectionObserver.mock.calls[0]
callback(mockedEntries); // test a callback 

这对我们有用

/* eslint-disable class-methods-use-this */

export default class {
  readonly root: Element | null;

  readonly rootMargin: string;

  readonly thresholds: ReadonlyArray<number>;

  constructor() {
    this.root = null;
    this.rootMargin = '';
    this.thresholds = [];
  }

  disconnect() {}

  observe() {}

  takeRecords(): IntersectionObserverEntry[] {
    return [];
  }

  unobserve() {}
}

然后安装

import MockIntersectionObserver from './MockIntersectionObserver';
window.IntersectionObserver = MockIntersectionObserver;