What Is the DOM?

7 min read

When a browser loads a web page, it doesn't just display the HTML and stop. It parses every tag, attribute, and text node into an in-memory tree of objects called the DOM — short for Document Object Model. Every test automation framework you'll ever use is, at its core, manipulating this tree. This lesson explains what the DOM is, why it exists, and why understanding it changes how you read every test failure.

HTML is text. The DOM is a tree.

The HTML you write looks like text:

<html>
  <head><title>Login</title></head>
  <body>
    <h1>Sign in</h1>
    <form>
      <input type="email" id="email" />
      <button type="submit">Submit</button>
    </form>
  </body>
</html>

The browser reads that text, parses it, and builds an in-memory tree. Every tag becomes a node. Every text becomes a node. Attributes become properties on those nodes. The whole tree is rooted at a single object called document. That tree — not the source text — is what JavaScript talks to.

The same page, drawn as the DOM tree it produces:

document
└── <html>
    ├── <head>
    │   └── <title> Login
    └── <body>
        ├── <h1> Sign in
        └── <form>
            ├── <input id="email">
            └── <button> Submit

Read top to bottom: document is the root. Below it sits the <html> element. Inside <html> are <head> and <body>. Inside <body> are the visible elements — heading, form, input, button. Each child knows its parent and its children. That parent-child structure is what you walk when you select elements.

How HTML becomes a DOM you can talk to

The HTML on disk is static. The DOM is live. JavaScript can change the tree, and the page updates immediately. That's how modern web apps work: a single-page React or Vue app is essentially a JavaScript program that mutates the DOM in response to user input and API responses.

Why QA engineers need to understand this

Every browser automation tool — Cypress, Playwright, Selenium, WebdriverIO — does the same thing under the hood: query the DOM, dispatch events to nodes, read properties off them. When cy.get('#email').type('alice@x.com') runs, the framework finds the <input> node, focuses it, and fires keyboard events.

When a test fails with "element not found," it isn't the framework being broken. It's the framework saying "I asked the DOM for that element, and the DOM doesn't have it right now." Understanding that changes how you debug.

The document object

Everything starts with document. It's a global object the browser exposes — the entry point to the entire DOM tree.

A few of the most-used reads:

document.title;            // "Login" — the <title> text
document.body;             // the <body> node
document.body.children;    // an HTMLCollection of body's direct children
document.URL;              // the page's URL
document.documentElement;  // the <html> node

You can try these right now. Open any page in Chrome, press F12, switch to the Console tab, and type each one in. The browser evaluates the expression against the live page.

Reading the DOM live, in DevTools

Two minutes in DevTools is worth more than any amount of reading. On any page:

  1. Open Chrome DevTools (F12 or right-click → Inspect).
  2. Go to the Console tab.
  3. Type document.title and press Enter. You'll see the page's title printed.
  4. Type document.body.children.length. The console returns a number — how many direct children the body has.
  5. Type document.querySelector("a") (we'll cover querySelector next lesson). The console returns the first <a> element on the page — and lets you click to highlight it.

Notice everything is live. Refresh the page and the values change. Click around and the DOM mutates beneath you. This is the surface every test automation tool talks to.

The DOM is live — and that's why timing is hard

Modern apps don't ship a finished DOM. They ship a small skeleton, then run JavaScript that adds, removes, and rearranges nodes in response to API calls and user events.

That's the source of half the flakiness in test automation:

  • A test runs cy.get("#submit-button") — but the button hasn't been rendered into the DOM yet (the API call is still in flight). Bug? No. Timing.
  • A test asserts expect(text).toBe("Saved") — but the success message was there, and the framework already removed it. Wrong moment.
  • A test types into an input — but the input was about to be re-rendered, and the typed text vanishes.

Every modern automation framework solves these problems with automatic retries and auto-waiting — concepts you'll meet in the last lesson of this chapter. The reason they need to exist is the live, asynchronous nature of the DOM.

⚠️ Common mistakes

  • Confusing the HTML source with the DOM. The "View source" menu shows the original HTML the server sent. The DOM tab in DevTools shows the current DOM, including everything JavaScript has added or changed. They're often very different — and your tests interact with the DOM, not the source.
  • Assuming an element is in the DOM just because it's on screen. Dialogs, dropdowns, and lazy-loaded panels often only exist in the DOM when they're open. A querySelector for the dropdown's content returns null until the user (or your test) opens it.
  • Forgetting that document.body.children is a live collection. It updates as the DOM changes. If you cache it before a JavaScript-driven re-render, the references you have may point at nodes that no longer exist.

🎯 Practice task

Explore a real DOM in DevTools. 10-15 minutes. (No file editing — just open a browser.)

  1. Open https://qa.codes (or any site with a fair bit of content) in Chrome.
  2. Press F12Console.
  3. Type each of the following and note the result:
    • document.title
    • document.URL
    • document.body.children.length
    • document.body.children[0] (the first child element of the body)
    • document.querySelector("h1") (we'll formalise querySelector next lesson — for now, observe what it returns and notice the highlight on the page)
  4. Right-click on any element on the page and choose Inspect. The Elements tab opens with that node highlighted in the DOM tree.
  5. In the Elements tab, double-click any text on the page and edit it — the page updates instantly. That's the live DOM in action.
  6. Stretch: find a button or link and type document.querySelector("button").click() in the console. The page reacts as if you'd clicked it. That's exactly what every automation framework does when it dispatches a click.

The next lesson covers the selectors — querySelector, getElementById, and friends — that locate the elements you want to read or interact with.

// tip to track lessons you complete and pick up where you left off across devices.