33

我的应用程序中有一个(编写得相当糟糕的)javascript 组件来处理无限滚动分页,我正在尝试重写它以使用,如此IntersectionObserver所述,但是我在测试它时遇到了问题。

有没有办法在 QUnit 测试中驱动观察者的行为,即用我的测试中描述的一些条目触发观察者回调?

我想出的一个可能的解决方案是在组件的原型中公开回调函数并在我的测试中直接调用它,如下所示:

InfiniteScroll.prototype.observerCallback = function(entries) {
    //handle the infinite scroll
}

InfiniteScroll.prototype.initObserver = function() {
    var io = new IntersectionObserver(this.observerCallback);
    io.observe(someElements);
}

//In my test
var component = new InfiniteScroll();
component.observerCallback(someEntries);
//Do some assertions about the state after the callback has been executed

我不太喜欢这种方法,因为它暴露了组件在IntersectionObserver内部使用的事实,这是我认为客户端代码不应该看到的实现细节,那么有没有更好的方法来测试它?

对不使用 jQuery 的解决方案的额外热爱 :)

4

7 回答 7

21

在您的jest.setup.js文件中,使用以下实现模拟 IntersectionObserver:

global.IntersectionObserver = class IntersectionObserver {
  constructor() {}

  disconnect() {
    return null;
  }

  observe() {
    return null;
  }

  takeRecords() {
    return null;
  }

  unobserve() {
    return null;
  }
};

除了使用Jest Setup File之外,您还可以直接在测试或 beforeAll,beforeEach 块中进行此模拟。

于 2019-07-30T11:42:31.430 回答
20

由于我们使用的 TypeScript 和 React (tsx) 的配置,所有发布的回答都没有对我有用。这是最终奏效的方法:

beforeEach(() => {
  // IntersectionObserver isn't available in test environment
  const mockIntersectionObserver = jest.fn();
  mockIntersectionObserver.mockReturnValue({
    observe: () => null,
    unobserve: () => null,
    disconnect: () => null
  });
  window.IntersectionObserver = mockIntersectionObserver;
});
于 2020-06-02T08:42:18.770 回答
17

这是基于先前答案的另一种选择,您可以在beforeEach方法内部或文件开头运行它.test.js

您还可以将参数传递setupIntersectionObserverMock给模拟observe和/或unobserve方法以使用jest.fn()模拟函数监视它们。

/**
 * Utility function that mocks the `IntersectionObserver` API. Necessary for components that rely
 * on it, otherwise the tests will crash. Recommended to execute inside `beforeEach`.
 * @param intersectionObserverMock - Parameter that is sent to the `Object.defineProperty`
 * overwrite method. `jest.fn()` mock functions can be passed here if the goal is to not only
 * mock the intersection observer, but its methods.
 */
export function setupIntersectionObserverMock({
  root = null,
  rootMargin = '',
  thresholds = [],
  disconnect = () => null,
  observe = () => null,
  takeRecords = () => [],
  unobserve = () => null,
} = {}) {
  class MockIntersectionObserver {
    constructor() {
      this.root = root;
      this.rootMargin = rootMargin;
      this.thresholds = thresholds;
      this.disconnect = disconnect;
      this.observe = observe;
      this.takeRecords = takeRecords;
      this.unobserve = unobserve;
    }
  }

  Object.defineProperty(window, 'IntersectionObserver', {
    writable: true,
    configurable: true,
    value: MockIntersectionObserver
  });

  Object.defineProperty(global, 'IntersectionObserver', {
    writable: true,
    configurable: true,
    value: MockIntersectionObserver
  });
}

对于 TypeScript:

/**
 * Utility function that mocks the `IntersectionObserver` API. Necessary for components that rely
 * on it, otherwise the tests will crash. Recommended to execute inside `beforeEach`.
 * @param intersectionObserverMock - Parameter that is sent to the `Object.defineProperty`
 * overwrite method. `jest.fn()` mock functions can be passed here if the goal is to not only
 * mock the intersection observer, but its methods.
 */
export function setupIntersectionObserverMock({
  root = null,
  rootMargin = '',
  thresholds = [],
  disconnect = () => null,
  observe = () => null,
  takeRecords = () => [],
  unobserve = () => null,
} = {}): void {
  class MockIntersectionObserver implements IntersectionObserver {
    readonly root: Element | null = root;
    readonly rootMargin: string = rootMargin;
    readonly thresholds: ReadonlyArray < number > = thresholds;
    disconnect: () => void = disconnect;
    observe: (target: Element) => void = observe;
    takeRecords: () => IntersectionObserverEntry[] = takeRecords;
    unobserve: (target: Element) => void = unobserve;
  }

  Object.defineProperty(
    window,
    'IntersectionObserver', {
      writable: true,
      configurable: true,
      value: MockIntersectionObserver
    }
  );

  Object.defineProperty(
    global,
    'IntersectionObserver', {
      writable: true,
      configurable: true,
      value: MockIntersectionObserver
    }
  );
}
于 2019-10-31T21:57:26.150 回答
13

2019年的同样问题,我就是这样解决的:

import ....

describe('IntersectionObserverMokTest', () => {
  ...
  const observeMock = {
    observe: () => null,
    disconnect: () => null // maybe not needed
  };

  beforeEach(async(() => {
    (<any> window).IntersectionObserver = () => observeMock;

    ....
  }));


  it(' should run the Test without throwing an error for the IntersectionObserver', () => {
    ...
  })
});

observe因此,我使用(and ) 方法创建了一个模拟对象,disconnect并覆盖IntersectionObserver了 window 对象。根据您的使用情况,您可能必须覆盖其他功能(请参阅:https ://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Browser_compatibility )

该代码的灵感来自https://gist.github.com/ianmcnally/4b68c56900a20840b6ca840e2403771c但不使用jest

于 2019-02-19T16:13:42.843 回答
2

我在基于 vue-cli 的设置中遇到了这个问题。我最终混合了上面看到的答案:

   const mockIntersectionObserver = class {
    constructor() {}
    observe() {}
    unobserve() {}
    disconnect() {}
  };

  beforeEach(() => {
    window.IntersectionObserver = mockIntersectionObserver;
  });
于 2020-06-04T15:50:00.517 回答
1

与@Kevin Brotcke 有类似的堆栈问题,除了使用他们的解决方案导致进一步的 TypeScript 错误:

缺少返回类型注释的函数表达式隐含地具有“任何”返回类型。

这是一个对我有用的经过调整的解决方案:

beforeEach(() => {
    // IntersectionObserver isn't available in test environment
    const mockIntersectionObserver = jest.fn()
    mockIntersectionObserver.mockReturnValue({
      observe: jest.fn().mockReturnValue(null),
      unobserve: jest.fn().mockReturnValue(null),
      disconnect: jest.fn().mockReturnValue(null)
    })
    window.IntersectionObserver = mockIntersectionObserver
  })
于 2020-09-01T09:16:03.537 回答
1

我对 Jest+Typescript 进行了这样的测试

         type CB = (arg1: IntersectionObserverEntry[]) => void;
            class MockedObserver {
              cb: CB;
              options: IntersectionObserverInit;
              elements: HTMLElement[];
            
              constructor(cb: CB, options: IntersectionObserverInit) {
                this.cb = cb;
                this.options = options;
                this.elements = [];
              }
            
              unobserve(elem: HTMLElement): void {
                this.elements = this.elements.filter((en) => en !== elem);
              }
            
              observe(elem: HTMLElement): void {
                this.elements = [...new Set(this.elements.concat(elem))];
              }
            
              disconnect(): void {
                this.elements = [];
              }
            
              fire(arr: IntersectionObserverEntry[]): void {
                this.cb(arr);
              }
            }
        
        function traceMethodCalls(obj: object | Function, calls: any = {}) {
          const handler: ProxyHandler<object | Function> = {
            get(target, propKey, receiver) {
              const targetValue = Reflect.get(target, propKey, receiver);
              if (typeof targetValue === 'function') {
                return function (...args: any[]) {
                  calls[propKey] = (calls[propKey] || []).concat(args);
                  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                  // @ts-ignore
                  return targetValue.apply(this, args);
                };
              } else {
                return targetValue;
              }
            },
          };
          return new Proxy(obj, handler);
        }

并且在测试中

describe('useIntersectionObserver', () => {
  let observer: any;
  let mockedObserverCalls: { [k: string]: any } = {};

  beforeEach(() => {
    Object.defineProperty(window, 'IntersectionObserver', {
      writable: true,
      value: jest
        .fn()
        .mockImplementation(function TrackMock(
          cb: CB,
          options: IntersectionObserverInit
        ) {
          observer = traceMethodCalls(
            new MockedObserver(cb, options),
            mockedObserverCalls
          );

          return observer;
        }),
    });
  });
  afterEach(() => {
    observer = null;
    mockedObserverCalls = {};
  });

    test('should do something', () => {
      const mockedObserver = observer as unknown as MockedObserver;
    
      const entry1 = {
        target: new HTMLElement(),
        intersectionRatio: 0.7,
      };
      // fire CB
      mockedObserver.fire([entry1 as unknown as IntersectionObserverEntry]);

      // possibly need to make test async/wait for see changes
      //  await waitForNextUpdate();
      //  await waitForDomChanges();
      //  await new Promise((resolve) => setTimeout(resolve, 0));


      // Check calls to observer
      expect(mockedObserverCalls.disconnect).toEqual([]);
      expect(mockedObserverCalls.observe).toEqual([]);
    });
});
于 2021-07-10T09:35:01.527 回答