What the DOM Really Is (and Why Your Tests Keep Breaking)

“DOM” is one of those words everyone uses and half the people misunderstand.

If you write UI tests and don’t truly get the DOM, you will write fragile tests. Period.

Let’s fix that.


DOM does NOT mean HTML

DOM = Document Object Model

HTML is a file.

The DOM is a live, in-memory structure created by the browser after loading that file.

Once the page is loaded, everything talks to the DOM, not the HTML:

  • JavaScript
  • CSS
  • Playwright
  • Screen readers

If you are selecting HTML, you are already too late.


The DOM is a tree, not a string

This HTML:

<button id="saveBtn">Save</button>

Becomes this DOM structure:

document
 └─ button
     ├─ id = "saveBtn"
     └─ text = "Save"

That button is now an object in memory, not text in a file.

When Playwright clicks, it clicks that object.


The DOM is dynamic (this is the killer detail)

The DOM changes all the time.

JavaScript can:

  • insert elements
  • remove elements
  • rename classes
  • wrap elements
  • move things around

Without reloading the page.

This means:

Your selector can be correct today and broken tomorrow, even if the feature still works.

That’s why tests rot.


Why DOM-based selectors are fragile

Example:

page.locator('button > span.label').click()

This breaks if:

  • a wrapper div is added
  • the span is removed
  • styles are refactored

None of these changes affect users.

All of them break your test.

That’s a bad trade.


DOM vs Accessibility Tree (critical difference)

Most people don’t know this part.

DOM

  • Structure-based
  • Tags, classes, IDs
  • Implementation detail

Accessibility Tree

  • Meaning-based
  • Roles, names, labels
  • What users and screen readers perceive

Playwright’s getByRole() uses the accessibility tree, not raw DOM structure.


Why this selector is better

❌ DOM-based:

page.locator('button:has-text("Save")').click()

✅ Accessibility-based:

page.getByRole('button', { name: 'Save' }).click()

Why this survives refactors:

  • Text “Save” stays
  • Role stays
  • DOM structure can change freely

Your test stays green.


IDs are not a silver bullet

This looks safe:

page.click('#saveBtn')

It isn’t.

IDs:

  • get renamed
  • get duplicated
  • get removed during redesigns
  • are often generated

They are implementation details, not user contracts.


How Playwright wants you to think

Playwright pushes you toward this mindset:

“Select elements the way users understand them.”

Users don’t see:

  • IDs
  • classes
  • DOM depth

They see:

  • buttons
  • labels
  • text
  • roles

That’s not an accident. It’s design.


The testing rule that actually matters

If your selector breaks when UI structure changes but user behavior doesn’t, the selector is wrong.


Practical takeaway

Use:

  • getByRole
  • getByLabel
  • getByText (carefully)

Avoid:

  • deep CSS selectors
  • structural selectors
  • styling-based selectors

DOM is an implementation detail.

User intent is the contract.


Final thought

Understanding the DOM is not about memorizing selectors.

It’s about knowing what is stable and what is not.

If your tests keep breaking, it’s usually not Playwright’s fault.

It’s your mental model.

Fix that, and everything else gets easier.