[JavaScript Series 18] How to Test DOM Functions

한국어 버전

Code that changes the DOM can be tricky and easily lead to bugs. This lesson shows how to verify UI logic with jsdom and lightweight simulations. The core is “jsdom + user scenario tests,” while BrowserMock is an optional helper.

Key terms

  1. jsdom: A library that emulates DOM APIs inside Node.js so you can run UI tests without a browser.
  2. User scenario test: Recreates what users click or type and checks the resulting UI.
  3. data-testid: A data- attribute that gives tests stable selectors.
  4. BrowserMock: A lightweight tool that spins up a fake browser from plain HTML strings.
  5. CI (Continuous Integration): Automates tests before code merges.

Core ideas

  • jsdom (Node-friendly DOM): Lets Node.js mimic browser APIs for render functions and event handlers.
  • User scenario test (replay user behavior): Script real interactions such as button clicks and form inputs.
  • BrowserMock/virtual DOM mock (HTML simulator): Helps you inspect HTML without a real browser.
  • Test hook data attribute (stable selector): Attributes like data-testid make element queries reliable.

Code examples

Install the tools that will provide our test environment. Before that, feel the pain point you are solving.

renderGreeting(root, "Minji");
console.log(root.innerHTML);

➡️ If “manually checking the console every time” annoyed you, you already see why automated tests matter.

npm install --save-dev vitest jsdom

Create a simple render function to give the tests a target.

// greet.js
export function renderGreeting(root, name) {
  root.innerHTML = `<p data-testid="greeting">Hello, ${name}</p>`;
}

Use jsdom to fake the DOM and verify the function.

// greet.test.js

describe("renderGreeting", () => {
  it("shows a greeting with the name", () => {
    const dom = new JSDOM(`<!doctype html><div id="root"></div>`);
    const root = dom.window.document.getElementById("root");
    renderGreeting(root, "Minji");
    const text = root.querySelector('[data-testid="greeting"]').textContent;
    expect(text).toBe("Hello, Minji");
  });
});

jsdom lets you manipulate DOM APIs in Node.js—think “handle the DOM without opening a browser.”

Now add a counter component with events.

// counter.js
export function initCounter(root) {
  let value = 0;
  root.innerHTML = `
    <div>
      <output data-testid="value">0</output>
      <button data-testid="inc">+</button>
    </div>
  `;
  root.querySelector('[data-testid="inc"]').addEventListener("click", () => {
    value += 1;
    root.querySelector('[data-testid="value"]').textContent = String(value);
  });
}

Recreate the user scenario by firing events yourself.

// counter.test.js

describe("initCounter", () => {
  it("increments the value when the button is clicked", () => {
    const dom = new JSDOM(`<!doctype html><div id="app"></div>`);
    const app = dom.window.document.getElementById("app");
    initCounter(app);
    const button = app.querySelector('[data-testid="inc"]');
    const value = app.querySelector('[data-testid="value"]');
    button.dispatchEvent(new dom.window.Event("click"));
    expect(value.textContent).toBe("1");
  });
});

This is the entire idea of a scenario test: “pretend a button was clicked and assert the value changed.”

BrowserMock is a lightweight option for quickly sketching HTML without a formal test runner—treat it as an optional helper.

// BrowserMock example

const { document, window } = mock("<form><input name=\"title\" /></form>");
const input = document.querySelector("input");
input.value = "Prep meeting";
document.querySelector("form").dispatchEvent(new window.Event("submit"));

BrowserMock builds a fake DOM from a string so setup is nearly instant.

Think of the BrowserMock workflow as:

  1. Create an HTML snippet as a string.
  2. Run the initialization function.
  3. Fire events that mimic user actions.
  4. Inspect document.body.innerHTML or any node’s text content.
http://localhost:5173/counter
TEST

Test Preview

What the test should verify

A test is not abstract rules; it checks what changes before and after the click.

jsdomdata-testidclick

Before Click

Initial state

output shows 0.

After Click

After one click

value switches to 1 while the button stays.

Assertion

Test focus

Ensure data-testid='value' reads 1.


const { document, window } = mock(`<div id="app"></div>`);
initCounter(document.getElementById("app"));
const button = document.querySelector('[data-testid="inc"]');
button?.dispatchEvent(new window.Event("click"));
console.log(document.querySelector('[data-testid="value"]').textContent);

Sharing the captured HTML (even with screenshots) helps teammates validate that BrowserMock output matches expectations. Use it when you need a quick visual confirmation.

Expected Output Snapshot

Testing articles benefit from showing npm test output.

:::terminal{title="Sample Vitest run", showFinalPrompt="false"}

[
  { "cmd": "npm test", "output": "✓ greet.test.js (1)\n✓ counter.test.js (1)\n\nTest Files  2 passed\nTests       2 passed\nDuration    0.83s", "delay": 500 }
]

:::

  • Check: Can you see which test files passed?
  • Check: If something fails, does the log tell you which file to fix?
  • Check: Do the DOM expectations line up with what the UI should display?

Why It Matters

  • DOM code hides subtle bugs when verified by eye alone. Tests let you refactor with confidence.
  • jsdom tests run quickly in CI.
  • Scenario tests double as step-by-step QA documentation.

Practice

  • Core: Write a jsdom test for a simple render function like renderGreeting.
  • Extension: Test components with events—click, input, and submit flows.
  • Debugging: Break a selector on purpose and confirm the test fails, then fix it.
  • Done: At least two tests pass and npm test succeeds in CI.

Wrap-Up

Even tiny UIs become safer once they have tests. Next we cover deployment and environment basics.

💬 댓글

이 글에 대한 의견을 남겨주세요