JSPM

  • Created
  • Published
  • Downloads 151138
  • Score
    100M100P100Q189448F
  • License MIT

A set of tools for emulating browser behavior in jsdom environment

Package Exports

  • jsdom-testing-mocks
  • jsdom-testing-mocks/dist/esm/index.js
  • jsdom-testing-mocks/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (jsdom-testing-mocks) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

jsdom-testing-mocks

A set of tools for emulating browser behavior in jsdom environment

Build status version PRs Welcome downloads MIT License Code of Conduct

GitHub Repo stars Twitter URL

Installation

npm i --D jsdom-testing-mocks

or

yarn add -D jsdom-testing-mocks

Mock viewport

Mocks matchMedia, allows testing of component's behavior depending on the viewport description (supports all of the Media Features). mockViewport must be called before rendering the component

Example, using React Testing Library:

import { mockViewport } from 'jsdom-testing-mocks';

it('shows the right lines on desktop and mobile', () => {
  const viewport = mockViewport({ width: '320px', height: '568px' });

  render(<TestComponent />);

  expect(
    screen.getByText('Content visible only on small screens')
  ).toBeInTheDocument();

  expect(
    screen.queryByText('Content visible only on large screens')
  ).not.toBeInTheDocument();

  act(() => {
    viewport.set({ width: '1440px', height: '900px' });
  });

  expect(
    screen.queryByText('Content visible only on small screens')
  ).not.toBeInTheDocument();

  expect(
    screen.getByText('Content visible only on large screens')
  ).toBeInTheDocument();

  viewport.cleanup();
});

Also, you can mock the viewport for a group of tests, using mockViewportForTestGroup:

import { mockViewportForTestGroup } from 'jsdom-testing-mocks'

describe('Desktop specific tests', () => {
  mockViewportForTestGroup({ width: '1440px', height: '900px' })

  test('this', () = {
    // ...
  })

  test('that', () = {
    // ...
  })
})

Mock IntersectionObserver

Provides a way of triggering intersection observer events

Example, using React Testing Library:

import { mockIntersectionObserver } from 'jsdom-testing-mocks';

const io = mockIntersectionObserver();

/*
Assuming html:
<div data-testid="container">
  <img src="..." alt="alt text" />
</div>

And an IntersectionObserver, observing the container
*/
it('loads the image when the component is in the viewport', () => {
  const { container } = render(<TestComponent />);

  expect(screen.queryByAltText('alt text')).not.toBeInTheDocument();

  // when the component's observed node is in the viewport - show the image
  act(() => {
    io.enterNode(screen.getByTestId('container'));
  });

  expect(screen.getByAltText('alt text')).toBeInTheDocument();
});

API

mockIntersectionObserver returns an object, that has several useful methods:

.enterNode(node, desc)

Triggers all IntersectionObservers observing the node, with isIntersected set to true and intersectionRatio set to 1. Other IntersectionObserverEntry params can be passed as desc argument, you can override any parameter except isIntersected

.leaveNode(node, desc)

Triggers all IntersectionObservers observing the node, with isIntersected set to false and intersectionRatio set to 0. Other IntersectionObserverEntry params can be passed as desc argument, you can override any parameter except isIntersected

.enterNodes(nodeDescriptions)

Triggers all IntersectionObservers observing the nodes in nodeDescriptions with multiple nodes entering at once. Each IntersectionObserver callback will receive only the nodes it's observing:

io.enterNodes([
  // you can pass multiple nodes each with its own state
  { node: screen.getByText('First Node'), desc: { intersectionRatio: 0.5 } },
  // description is optional:
  { node: screen.getByText('Second Node') },
  // or you can use a shorthand:
  screen.getByText('Third Node'),
]);

.leaveNodes(nodeDescriptions)

Triggers all IntersectionObservers observing the nodes in nodeDescriptions with multiple nodes leaving at once. Each IntersectionObserver callback will receive only the nodes it's observing.

.triggerNodes(nodeDescriptions)

Triggers all IntersectionObservers observing the nodes in nodeDescriptions with multiple nodes at once with custom descriptions (isIntersected is not enforced). Each IntersectionObserver callback will receive only the nodes it's observing

.enterAll(desc) and .leaveAll(desc)

Triggers all IntersectionObservers for each of the observed nodes

Mock ResizeObserver

Provides a way of triggering resize observer events. It's up to you to mock elements' sizes. If your component uses contentRect provided by the callback, you must mock element's getBoundingClientRect (for exemple using a helper function mockElementBoundingClientRect provided by the lib)

Currently the mock doesn't take into account multi-column layouts, so borderBoxSize and contentBoxSize will contain only one full-sized item

Example, using React Testing Library:

import {
  mockResizeObserver,
  mockElementBoundingClientRect,
} from 'jsdom-testing-mocks';

const DivWithSize = () => {
  const [size, setSize] = useState({ width: 0, height: 0 });
  const ref = useRef(null);

  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      setSize({
        width: entries[0].contentRect.width,
        height: entries[0].contentRect.height,
      });
    });

    observer.observe(ref.current);

    return () => {
      observer.disconnect();
    };
  }, []);

  return (
    <div data-testid="theDiv" ref={ref}>
      {size.width} x {size.height}
    </div>
  );
};

const resizeObserver = mockResizeObserver();

it('prints the size of the div', () => {
  render(<DivWithSize />);

  const theDiv = screen.getByTestId('theDiv');

  expect(screen.getByText('0 x 0')).toBeInTheDocument();

  mockElementBoundingClientRect(theDiv, { width: 300, height: 200 });

  act(() => {
    resizeObserver.resize(theDiv);
  });

  expect(screen.getByText('300 x 200')).toBeInTheDocument();

  mockElementBoundingClientRect(theDiv, { width: 200, height: 500 });

  act(() => {
    resizeObserver.resize(theDiv);
  });

  expect(screen.getByText('200 x 500')).toBeInTheDocument();
});

API

mockResizeObserver returns an object, that has one method:

.resize(elements: HTMLElement | HTMLElement[])

Triggers all resize observer callbacks for all observers that observe the passed elements

Mock Web Animations API

Warning: experimental, bug reports, tests and feedback are greatly appreciated

Mocks WAAPI functionality using requestAnimationFrame. With one important limitation — there are no style interpolations. Each frame applies the closest keyframe from list of passed keyframes or a generated "initial keyframe" if only one keyframe is passed (initial keyframe removes/restores all the properties set by the one keyframe passed). As the implementation is based on the official spec it should support the majority of cases, but the test suite is far from complete, so here be dragons

Example, using React Testing Library:

import { mockAnimationsApi } from 'jsdom-testing-mocks';

const TestComponent = () => {
  const [isShown, setIsShown] = useState(false);

  return (
    <div>
      {/* AnimatePresence is a component that adds its children in the dom
          and fades it in using WAAPI, with 2 keyframes: [{ opacity: 0 }, { opacity: 1 }],
          also adding a div with the word "Done!" after the animation has finished
          You can find implementation in examples
       */}
      <AnimatePresence>{isShown && <div>Hehey!</div>}</AnimatePresence>
      <button
        onClick={() => {
          setIsShown(true);
        }}
      >
        Show
      </button>
    </div>
  );
};

mockAnimationsApi();

it('adds an element into the dom and fades it in', async () => {
  render(<TestComponent />);

  expect(screen.queryByText('Hehey!')).not.toBeInTheDocument();

  await userEvent.click(screen.getByText('Show'));

  // assume there's only one animation present in the document at this point
  // in practice it's better to get the running animation from the element itself
  const element = screen.getByText('Hehey!');
  const animation = document.getAnimations()[0];

  // our AnimatePresence implementation has 2 keyframes: opacity: 0 and opacity: 1
  // which allows us to test the visibility of the element, the first keyframe
  // is applied right after the animation is ready
  await animation.ready;

  expect(element).not.toBeVisible();

  // this test will pass right after 50% of the animation is complete
  // because this mock doesn't interpolate keyframes values,
  // but chooses the closest one at each frame
  await waitFor(() => {
    expect(element).toBeVisible();
  });

  // AnimatePresence will also add a div with the text 'Done!' after animation is complete
  await waitFor(() => {
    expect(screen.getByText('Done!')).toBeInTheDocument();
  });
});

Using with fake timers

It's perfectly usable with fake timers, except for the issue with promises. Also note that you would need to manually advance timers by the duration of the animation taking frame duration (which currently is set to 16ms in jest/sinon.js) into account. So if you, say, have an animation with a duration of 300ms, you will need to advance your timers by the value that is at least the closest multiple of the frame duration, which in this case is 304ms (19 frames * 16ms). Otherwise the last frame may not fire and the animation won't finish.

Current issues

  • No support for steps easings
  • Needs more tests