测试使用 IntersectionObserver 的代码

问题描述 投票:0回答:8

我的应用程序中有一个处理无限滚动分页的 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)?

javascript unit-testing qunit intersection-observer
8个回答
59
投票

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

31
投票

这是基于之前答案的另一种选择,您可以在

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

31
投票

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

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

  disconnect() {
    return null;
  }

  observe() {
    return null;
  }

  takeRecords() {
    return null;
  }

  unobserve() {
    return null;
  }
};

您也可以直接在测试或 beforeAll、beforeEach 块中直接进行此模拟,而不是使用 Jest 设置文件


15
投票

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
(和
disconnect
)方法创建一个模拟对象,并覆盖窗口对象上的
IntersectionObserver
。根据您的使用情况,您可能需要覆盖其他函数(请参阅:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Browser_compatibility

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

jest


4
投票

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

2
投票

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

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

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

2
投票

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

0
投票

在我的测试中,我实际上想触发传递给new

IntersectionObserver
的回调函数。扩展上面的示例,我首先编写自己的类,将
callback
存储在静态变量中:

class MockIntersectionObserver implements IntersectionObserver {
  static callback: IntersectionObserverCallback;
  options: IntersectionObserverInit | undefined;
  constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
    MockIntersectionObserver.callback = callback;
    this.options = options;
  }

  root: Document | Element | null = null;
  rootMargin: string = '';
  thresholds: readonly number[] = [];
  disconnect = vi.fn();
  observe = vi.fn();
  takeRecords = vi.fn();
  unobserve = vi.fn();

  static triggerCallback(entries: any) {
    MockIntersectionObserver.callback(entries, {
      disconnect: vi.fn(),
      observe: vi.fn(),
      takeRecords: vi.fn(),
      unobserve: vi.fn(),
      root: null,
      rootMargin: '',
      thresholds: [],
    });
  }
}
window.IntersectionObserver = MockIntersectionObserver;

在测试中我会像这样手动触发回调:

MockIntersectionObserver.triggerCallback([{ isIntersecting: true }]);
© www.soinside.com 2019 - 2024. All rights reserved.