JS - 测试使用IntersectionObserver的代码

时间:2017-05-29 20:34:23

标签: javascript unit-testing qunit intersection-observer

我的应用程序中有一个(写得很差)javascript组件处理无限滚动分页,我正在尝试重写它以使用IntersectionObserver,如here所述,但是我我在测试方面遇到了问题。

有没有办法在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的解决方案的热爱:)

7 个答案:

答案 0 :(得分:6)

由于我们正在使用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;
});

答案 1 :(得分:5)

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

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

  observe() {
    return null;
  }

  unobserve() {
    return null;
  }
};

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

答案 2 :(得分:1)

2019年的同样问题是我解决的方法:

import ....

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

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

    ....
  }));


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

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

该代码受https://gist.github.com/ianmcnally/4b68c56900a20840b6ca840e2403771c的启发,但未使用jest

答案 3 :(得分:1)

我在基于vue-cli的安装程序中遇到了这个问题。我最终混合使用了上面看到的答案:

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

  beforeEach(() => {
    window.IntersectionObserver = mockIntersectionObserver;
  });

答案 4 :(得分: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([]);
    });
});

答案 5 :(得分:0)

这是基于先前答案的另一种选择,您可以在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 {object} 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({
  observe = () => null,
  unobserve = () => null,
} = {}) {
  class IntersectionObserver {
    observe = observe;
    unobserve = unobserve;
  }
  Object.defineProperty(
    window,
    'IntersectionObserver',
    { writable: true, configurable: true, value: IntersectionObserver }
  );
  Object.defineProperty(
    global,
    'IntersectionObserver',
    { writable: true, configurable: true, value: IntersectionObserver }
  );
}

答案 6 :(得分:0)

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

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

以下是对我有用的调整后的解决方案:

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