1

I've read all of the relevant questions on this topic, and I realize this will probably be marked as a duplicate, but I simply cannot for the life of me figure out how to get this working.

I have this simple function that lazily loads elements:

export default function lazyLoad(targets, onIntersection) {
  const observer = new IntersectionObserver((entries, self) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        onIntersection(entry.target);
        self.unobserve(entry.target);
      }
    });
  });

  document.querySelectorAll(targets).forEach((target) => observer.observe(target));
  return observer;
}

Example usage:

lazyLoad('.lazy-img', (img) => {
  const pictureElement = img.parentElement;
  const source = pictureElement.querySelector('.lazy-source');

  source.srcset = source.getAttribute('data-srcset');
  img.src = img.getAttribute('data-src');
});

Now, I'm trying to test the lazyLoad function using jest, but I obviously need to mock IntersectionObserver since it's a browser API, not a native JavaScript one.

The following works for testing the observe method:

let observe;
let unobserve;

beforeEach(() => {
  observe = jest.fn();
  unobserve = jest.fn();

  window.IntersectionObserver = jest.fn(() => ({
    observe,
    unobserve,
  }));
});

describe('lazyLoad utility', () => {
  it('calls observe on each target', () => {
    for (let i = 0; i < 3; i++) {
      const target = document.createElement('img');
      target.className = 'lazy-img';
      document.body.appendChild(target);
    }

    lazyLoad(
      '.lazy-img',
      jest.fn(() => {})
    );

    expect(observe).toHaveBeenCalledTimes(3);
  });
});

But I also want to test the .isIntersecting logic, where the callback fires... Except I don't know how to do that. How can I test intersections with jest?

2 Answers2

5

Mocking stuff is so easy when you pass it as an argument:

export default function lazyLoad(targets, onIntersection, observerClass = IntersectionObserver) {
  const observer = new observerClass(...)
  ...
}


// test file
let entries = [];
const observeFn = jest.fn();
const unobserveFn = jest.fn()
class MockObserver {
  constructor(fn) {
    fn(entries,this);
  }

  observe() { observeFn() }
  unobserve() { unobserveFn() }
}

test('...',() => {
   // set `entries` to be something so you can mock it
   entries = ...something
   lazyLoad('something',jest.fn(),MockObserver);
});
Adam Jenkins
  • 51,445
  • 11
  • 72
  • 100
  • This makes sense, thank you! I didn't think to use dependency injection like this. I assume I'll need `@babel/plugin-proposal-class-properties`, though, right? Because I can't do `observe = jest.fn()`. – Aleksandr Hovhannisyan Oct 01 '20 at 14:37
  • @AleksandrH I changed the syntax, the dependency injection was the main idea, for the rest of the spies just do whatever you need to do. – Adam Jenkins Oct 01 '20 at 14:40
  • 1
    @AleksandrH let this be one of those standout moments for how to write testable code - whatever you need to mock, just make it an argument. – Adam Jenkins Oct 01 '20 at 14:41
  • It sure was a lightbulb moment for me, thank you! My tests are much easier to write now for this utility. – Aleksandr Hovhannisyan Oct 01 '20 at 14:46
2

Another option is to use mockObserver mentioned above and mock in window.

window.IntersetionObserver = mockObserver

And you don't necessary need to pass observer in component props.

The important point to test if entries isIntesecting is to mock IntersectionObserver as class like mentioned above.

Heron Eto
  • 61
  • 2