Skip to content

Jest DOM Testing

Not every frontend project uses React or Vue — plenty of code manipulates the DOM directly. This lesson covers testing that kind of code using Jest's jsdom environment and the jest-dom matcher library.

Manipulating and Asserting on the DOM Directly

With testEnvironment: 'jsdom', document and window are available globally in your tests, letting you build up DOM structure, call the function under test, and then query the resulting DOM directly — no framework required.

This pattern is common for testing vanilla JavaScript widgets, small UI utilities, or legacy jQuery-adjacent code that manipulates document directly.

// toggleVisibility.js
function toggleVisibility(selector) {
  const el = document.querySelector(selector);
  el.classList.toggle('hidden');
}
module.exports = { toggleVisibility };

// toggleVisibility.test.js
const { toggleVisibility } = require('./toggleVisibility');

test('toggles the hidden class', () => {
  document.body.innerHTML = '<div id="panel" class="hidden"></div>';

  toggleVisibility('#panel');

  expect(document.getElementById('panel')).not.toHaveClass('hidden');
});

Setting document.body.innerHTML directly is a simple way to build test fixtures for vanilla DOM code.

Extra Matchers from jest-dom

expect(element).toBeVisible();
expect(element).toHaveTextContent('Hello');
expect(element).toHaveClass('active');
expect(element).toBeDisabled();
expect(element).toHaveAttribute('aria-expanded', 'true');
  • @testing-library/jest-dom adds DOM-specific matchers on top of Jest's defaults.
  • These matchers produce much clearer failure messages than manually checking element.className.includes(...).
  • Import '@testing-library/jest-dom' once in a setupFilesAfterEnv file to use these matchers everywhere.
  • Works with plain DOM elements, not just elements rendered by React/Vue.

DOM Testing Cheat Sheet

Common jest-dom matchers for asserting on DOM elements.

Matcher Checks
.toBeVisible() Element is rendered and not hidden
.toBeDisabled() / .toBeEnabled() Form element's disabled state
.toHaveTextContent(text) Element's text content matches
.toHaveClass(name) Element has a specific CSS class
.toHaveAttribute(name, value?) Element has an attribute, optionally with a value
.toBeInTheDocument() Element exists in the document
.toHaveValue(value) Form input's current value

Building DOM Fixtures for a Test

For vanilla DOM code, setting document.body.innerHTML to a fixed HTML string is the simplest way to set up a realistic starting structure before calling the function under test — no rendering library required.

beforeEach(() => {
  document.body.innerHTML = `
    <form id="signup">
      <input name="email" />
      <button type="submit">Sign Up</button>
    </form>
  `;
});

Understanding jsdom's Limitations

jsdom implements most DOM and browser APIs, but not all of them — things like real layout/rendering (getBoundingClientRect returns zeros by default), canvas rendering, and some newer browser APIs are not fully supported. For code that depends heavily on real layout or rendering, consider an actual browser-based tool like Playwright or Cypress instead.

  • Layout measurements (getBoundingClientRect, offsetWidth) return default/zero values.
  • window.scrollTo, alert, and some browser dialogs are stubbed but don't do anything real.
  • For real visual/layout verification, use an actual browser-based E2E tool instead.

Common Mistakes

  • Expecting real layout measurements (getBoundingClientRect) to return meaningful values in jsdom.
  • Not resetting document.body.innerHTML between tests, leaking DOM state across test cases.
  • Manually checking element.className instead of using the clearer .toHaveClass() matcher.
  • Trying to test complex visual behavior with jsdom instead of a real browser-based E2E tool.

Key Takeaways

  • jsdom lets you test vanilla DOM manipulation code without any UI framework.
  • jest-dom matchers provide clear, purpose-built assertions for DOM elements.
  • Setting document.body.innerHTML is a simple way to build test fixtures.
  • jsdom doesn't implement real layout/rendering; use a real browser tool for that.

Pro Tip

Reset document.body.innerHTML = '' in an afterEach() for any test file that manipulates the DOM directly — it prevents leftover elements from one test silently affecting the next.