21

I have the following method:

    componentDidLoad() {
        this.image = this.element.shadowRoot.querySelector('.lazy-img');
        this.observeImage();
      }

    observeImage = () => {
        if ('IntersectionObserver' in window) {
          const options = {
            rootMargin: '0px',
            threshold: 0.1
          };
    
          this.observer = new window.IntersectionObserver(
            this.handleIntersection,
            options
          );
    
          this.observer.observe(this.image);
        } else {
          this.image.src = this.src;
        }
      };

and I trying to test for IntersectionObserver.observe call like this:

    it('should create an observer if IntersectionObserver is available', async () => {
        await newSpecPage({
          components: [UIImageComponent],
          html: `<ui-image alt="Lorem ipsum dolor sit amet" src="http://image.example.com"></ui-image>`
        });
    
        const mockObserveFn = () => {
          return {
            observe: jest.fn(),
            unobserve: jest.fn()
          };
        };
    
        window.IntersectionObserver = jest
          .fn()
          .mockImplementation(mockObserveFn);
    
        const imageComponent = new UIImageComponent();
        imageComponent.src = 'http://image.example.com';
    
        const mockImg = document.createElement('img');
        mockImg.setAttribute('src', null);
        mockImg.setAttribute('class', 'lazy-img');
    
        imageComponent.element.shadowRoot['querySelector'] = jest.fn(() => {
          return mockImg;
        });
        expect(imageComponent.image).toBeNull();
        imageComponent.componentDidLoad();
    
        expect(mockObserveFn['observe']).toHaveBeenCalled();
      });

But can't make it work, my mockObserveFn.observe have not been call, any suggestion

adrisons
  • 3,443
  • 3
  • 32
  • 48
Jean
  • 5,201
  • 11
  • 51
  • 87

5 Answers5

30

Your mockObserveFn.observe has not been called, because it does not exists.

probably you're getting the following error:

Matcher error: received value must be a mock or spy function

You can define your mock like this

const observe = jest.fn();
const unobserve = jest.fn();

// you can also pass the mock implementation
// to jest.fn as an argument
window.IntersectionObserver = jest.fn(() => ({
  observe,
  unobserve,
}))

and then you can expect:

expect(observe).toHaveBeenCalled();
Teneff
  • 30,564
  • 13
  • 72
  • 103
16

This solution works for me.

Basicly you just put IntersectionMock inside beforeEach

beforeEach(() => {
  // IntersectionObserver isn't available in test environment
  const mockIntersectionObserver = jest.fn();
  mockIntersectionObserver.mockReturnValue({
    observe: () => null,
    unobserve: () => null,
    disconnect: () => null
  });
  window.IntersectionObserver = mockIntersectionObserver;
});
Yudi Krisnandi
  • 417
  • 1
  • 5
  • 12
1

Instead of these custom solutions, I suggest using a well-maintained npm library jsdom-testing-mocks. Here's how to use it -

import { screen, render } from '@testing-library/react'
import { mockIntersectionObserver } from 'jsdom-testing-mocks'
import React from 'react'

import App from './App'

mockIntersectionObserver()

describe('Test App component', () => {
  it('renders App heading', () => {
    render(<App />)
    const h1Elem = screen.getByText(/app heading/i)
    expect(h1Elem).toBeInTheDocument()
  })
})
sohammondal
  • 635
  • 6
  • 12
0

Shouldn't actually call the handler but thats how I could trigger it since we cant really trigger a view Give your observed element intersectionRatio

const observeMock = jest.fn();
const disconnectMock = jest.fn();

beforeEach(() => {
    const mockIntersectionObserver = class IO {
        constructor(handler) {
            this.handler = handler;
        }

        observe(element) {
            observeMock();
            this.handler([{
                intersectionRatio: element.intersectionRatio,
                target: element
            }]);
        }

        disconnect() {
            disconnectMock();
        }
    };
    window.IntersectionObserver = mockIntersectionObserver;
});
yairniz
  • 408
  • 5
  • 7
0

Just another option:

const createDOMRect = ({
    x = 0,
    y = 0,
    top = 0,
    right = 0,
    bottom = 0,
    left = 0,
    width = 0,
    height = 0,
}: Partial<DOMRect> = {}) =>
    <DOMRect>{
        x,
        y,
        top,
        right,
        bottom,
        left,
        width,
        height,
    };

const createIntersectionObserverEntry = (
    target: Element,
    root: Element | Document | null,
): IntersectionObserverEntry => {
    const rootElement = root as Element;
    const rootBounds = rootElement?.getBoundingClientRect() ?? null;
    const boundingClientRect = target.getBoundingClientRect();
    const intersectionRatio = boundingClientRect.width * boundingClientRect.height > 0 ? 1 : 0;
    const intersectionRect = <DOMRectReadOnly>createDOMRect();
    const isIntersecting = intersectionRatio > 0;
    const time = performance.now();

    return {
        boundingClientRect,
        intersectionRatio,
        intersectionRect,
        isIntersecting,
        rootBounds,
        target,
        time,
    };
};

class IntersectionObserverMock {
    public readonly root: null | Element | Document;
    public readonly rootMargin: string;
    public readonly thresholds: ReadonlyArray<number>;
    private readonly callback: IntersectionObserverCallback;
    private readonly entries: IntersectionObserverEntry[] = [];

    constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
        this.callback = callback;
        this.root = options?.root ?? null;
        this.rootMargin = options?.rootMargin ?? '0px';
        this.thresholds = options?.threshold instanceof Array ? options.threshold : [options?.threshold ?? 0];
    }

    observe(target: Element) {
        const entry = createIntersectionObserverEntry(target, this.root);
        this.entries.push(entry);
        this.callback(this.entries, this);
    }

    unobserve(target: Element) {
        const index = this.entries.findIndex(entry => entry.target === target);
        if (index !== -1) {
            this.entries.splice(index, 1);
        }
    }

    disconnect() {
        this.entries.length = 0;
    }

    takeRecords(): IntersectionObserverEntry[] {
        return this.entries.splice(0);
    }

    // if you prefer...
    $simulateIntersection(target: Element, root: null | Element | Document) {
        const entry = createIntersectionObserverEntry(target, root ?? this.root);
        this.callback([entry], this);
    }

}

export default IntersectionObserverMock;

and then

import IntersectionObserverMock from './IntersectionObserverMock';

let originalIntersectionObserver: IntersectionObserver;
let instance: IntersectionObserver;
let callback: IntersectionObserverCallback;
let observe: unknown;
let unobserve: unknown;
let disconnect: unknown;
let takeRecords: unknown;

beforeEach(() => {
    callback = jest.fn();
    instance = new IntersectionObserverMock(callback, {
        root: null,
        rootMargin: '0px',
        threshold: 1.0,
    });
    observe = jest.spyOn(instance, 'observe');
    unobserve = jest.spyOn(instance, 'unobserve');
    takeRecords = jest.spyOn(instance, 'takeRecords');
    disconnect = jest.spyOn(instance, 'disconnect');

    originalIntersectionObserver = global.IntersectionObserver;
    Object.defineProperty(global, 'IntersectionObserver', {
        writable: true,
        value: jest.fn(() => instance),
    });
});

afterEach(() => {
    Object.defineProperty(global, 'IntersectionObserver', {
        value: originalIntersectionObserver,
        writable: true,
    });
});
Adrian Miranda
  • 315
  • 3
  • 9