The DOM
The Document Object Model (DOM) is the browser’s live, in-memory model of a web page. When the browser parses HTML it builds a tree of nodes — the document at the root, an element node for every element, text nodes for the words — and gives JavaScript an interface to read and change that tree. The browser draws the screen from the DOM and the CSS, not from the HTML file.
A concrete example: a cart page sends <input id="qty" value="1">. The DOM gets an input element whose value attribute is 1. When the user changes the quantity to 3, the element’s value property becomes 3, the attribute still says 1, and the page source does not change at all. The source is the page as it was sent; the DOM is the page as it is now.
Why it matters
Everything a UI test does goes through the DOM. A locator such as #qty is a query against the current tree, a click is delivered to an element node, and an assertion reads a property or the text of a node. If you do not know what the DOM held at that moment, you cannot tell whether a failed test found a defect, used a wrong locator or looked too early.
It matters for manual testing too. Some defects are visible only in the DOM: a message that is added but stays hidden, a field whose label is not connected to it, a button that exists twice. Reading the DOM lets you report the cause — the element, its attributes and its state — instead of only the symptom, and check whether a problem is in the page or in your test.
A tree of nodes
In the lesson on how browsers work you saw the browser parse HTML. The output of that step is the DOM: a tree in which every item is a node. The document node is the root, available to scripts as document. Every HTML element becomes an element node, a branch that can contain other nodes. The text inside elements becomes text nodes, the leaves of the tree. HTML comments become comment nodes: they are in the DOM even though nothing displays them.
Attributes are not children in the tree. id="qty" is stored on the input element itself, like a label stuck on a box rather than an item inside it. The spaces and line breaks between tags usually become text nodes too, which is why a script that counts an element’s child nodes often finds more than the tags suggest.
- Parent and child: in the form below, the
formis the parent of thelabel,input,buttonandp, and they are its children. - Siblings: children of the same parent, such as the
labeland theinput. - Ancestor and descendant:
bodyis an ancestor of theinput; everything inside the form is a descendant of the form. - Locators use the same relationships. In CSS,
form > inputmeans an input that is a child of a form, andform inputmeans any descendant input. In XPath,/steps to a child,//searches all descendants (at the start of an expression, the whole document),[@id='qty']keeps only elements with that attribute value, and..steps up to the parent://input/..moves from the input to its parent.
<form id="cart">
<label for="qty">Quantity</label>
<input id="qty" value="1">
<button type="submit">Update</button>
<p class="msg" hidden>Cart updated</p>
</form>value="1" sit on their element, not inside it.The cart form in the DOM
Part 1:#document └ html ├ head └ body └ Part 2:form id="cart" ├ label for="qty" │ └ Part 4:"Quantity" ├ input Part 3:id="qty" value="1" ├ button type="submit" │ └ Part 4:"Update" └ p class="msg" Part 5:hidden └ Part 4:"Cart updated"
- 1Document nodeThe root,
documentin scripts: every search for an element starts here or from an element already found - 2Element nodeOne for each element, from
htmltop. Theformis the parent of four children, which are siblings of each other - 3AttributesStored on the element, not children in the tree. After the user changes the quantity to 3, the
valueproperty is "3" while this attribute still says "1" - 4Text nodesThe words a user reads; leaves with no children. Text locators match them
- 5Present, not displayed
hiddenkeeps the paragraph off the screen, but a query still finds it: present is not visible
The head’s children and the whitespace-only text nodes between tags are left out.
Attributes and properties
An attribute is written in the HTML: value="1". A property belongs to the JavaScript object that represents the element: qty.value. For many attributes the browser keeps the two in step, which the HTML standard calls reflecting: setting the id property changes the id attribute, and the other way round. Some pairs have different names: the class attribute is the className property, and for is htmlFor.
The pairs that matter most to a tester are the ones that do not stay in step:
valueon an input: the attribute is the starting value, also readable as thedefaultValueproperty. Thevalueproperty is what the field holds now. Typing changes the property only.checkedon a checkbox: the attribute says whether it starts ticked (defaultChecked); thecheckedproperty says whether it is ticked now.hrefon a link:getAttribute('href')returns what the HTML says, such as/help; thehrefproperty returns the full URL the browser resolved, such ashttps://shop.example/help.- Boolean attributes such as
disabledandrequired: only presence counts.disabled="false"still disables a button, because the attribute is there.hiddenbehaves the same way: with any value, evenhidden="false", the element is hidden.
// After the user changed the quantity from 1 to 3
const qty = document.querySelector('#qty');
qty.getAttribute('value'); // "1" attribute: what the HTML said
qty.defaultValue; // "1" property that reflects the attribute
qty.value; // "3" property: what the field holds nowHow JavaScript changes the DOM
The JavaScript from the previous lesson changes a page by changing the DOM; the browser then redraws the screen from the new tree. Almost every change is one of a few operations: find a node, create one, insert it, remove it, or change its text, attributes or properties. Scripts usually find elements with document.querySelector(), which takes a CSS selector and returns the first matching element, or null when nothing matches; querySelectorAll() returns all the matches.
Frameworks such as React, Angular and Vue make the same calls for you. When data changes they may remove an element and insert a new one in its place, so an element your test found a moment ago may no longer be in the tree. Selenium then fails with a stale element reference error; Playwright avoids most of these failures because it locates an up-to-date element every time a locator is used.
- The page source and the DOM differ. View Page Source (Ctrl+U) shows what the server sent; the DOM also contains everything scripts added since. In a single-page application the source can be little more than an empty
<div id="root"></div>. - Content can arrive after the page appears. When a script fetches data and then inserts it, the element does not exist at first, so a test has to wait for it.
- The parser changes the tree too. A table row written directly inside
<table>ends up inside a<tbody>that the parser inserts, so an XPath copied from the source, such as//table/tr, matches nothing. - Text inserted with
innerHTMLis parsed as markup, whiletextContentinserts plain text. If a name typed as<b>Ali</b>shows up in bold somewhere, user input is reaching the DOM as markup: report it, because the same path can allow cross-site scripting (XSS).
const form = document.querySelector('#cart'); // find an element
const msg = form.querySelector('.msg');
msg.textContent = 'Cart updated: 3 items'; // replace its text
msg.hidden = false; // removes the hidden attribute
const note = document.createElement('p'); // create a node, not in the tree yet
note.textContent = 'Free delivery';
form.append(note); // insert it: now it can be seen
note.remove(); // remove it againInspecting the DOM, and why locators depend on it
Right-click an element and choose Inspect: the developer tools open on the Elements panel (Inspector in Firefox) with that element selected. This panel shows the live DOM, not the source: elements added by scripts appear, removed ones disappear, and attributes update as they change. Its tree shows attributes, not properties, so after you change the quantity to 3 it still shows value="1". To read a property, type $0.value in the Console; $0 is the element selected in the Elements panel.
tbody the parser inserted; the Elements tree shows both but lists the value attribute, so of the three only the Console ($0.value) reads what a user typed.View Page SourceCtrl+U
The HTML as the server sent it
- Elements added by scripts:
- No
- A
tbodythe parser inserted: - No
- What the user typed into a field:
- No
- Use it to:
- See what the server delivered
Elements panel
The live DOM: the nodes and their attributes now
- Elements added by scripts:
- Yes
- A
tbodythe parser inserted: - Yes
- What the user typed into a field:
- No: it shows the
valueattribute - Use it to:
- Write and debug locators
Console$0
Any property of the element selected in the Elements panel
- Elements added by scripts:
- Yes
- A
tbodythe parser inserted: - Yes
- What the user typed into a field:
- Yes:
$0.value - Use it to:
- Read properties and count locator matches
Every locator is a query against this live tree. CSS selectors and XPath match element names, attributes and positions; text locators match the text of nodes; role-based locators such as Playwright’s getByRole use a role and an accessible name computed from the element, its attributes, its text and related elements such as its <label>. That is why you write and debug locators against the DOM in the state the test will meet, never against the page source.
- Count matches before you rely on a locator:
document.querySelectorAll('#cart button').lengthin the Console should return 1. In Chrome and Edge, Ctrl+F in the Elements panel also accepts a CSS selector or an XPath expression. - Check in the right state: after the data has loaded, with the dialog open, in every language the suite runs in.
- Present is not visible. The
pwithhiddenis in the DOM, so a query finds it, but the user cannot see it. When the requirement is that the user sees something, assert visibility. - An iframe holds a separate document, and a shadow root hides a component’s inner tree from ordinary queries. Both need the special handling described in the locator strategies guide.
Worked example: three checks on the cart page
A tester automates the cart form from this lesson, together with an order history table. The requirement: after the user changes the quantity and clicks Update, the message "Cart updated" is shown. Three checks give surprising results, and repeating each one in the developer tools shows how the DOM explains it.
- Check 1 fails: after the quantity is changed to 3, the test reads the input’s
valueattribute and gets "1". Repeat it in the Console with the input selected in the Elements panel:$0.getAttribute('value')gives "1", while$0.valuegives "3". The application is right; the test read the attribute instead of the property. Fix: make the check read thevalueproperty. Automation tools expose the same attribute and property pair under their own names, and the Playwright and Selenium lessons show which call reads which. - Check 2 fails:
//table[@id='orders']/tr, copied from the page source, finds no rows. In the Console,$x()runs an XPath expression and returns the matching elements:$x("//table[@id='orders']/tr")returns an empty list[], and the Elements panel shows thetbodythe parser inserted. Fix: locate the rows as they are in the DOM:$x("//table[@id='orders']/tbody/tr")returns every row. - Check 3 passes, but should not: the test reads the
textContentof.msgand finds "Cart updated: 3 items". The tester watching the run sees no message. In the Elements panel thepstill carrieshidden, and with it selected,$0.hiddenin the Console returnstrue: the script changed the text but never showed it. This is a real defect, and the test missed it because it checked text, not visibility. - To see exactly what a script changed, copy the form’s outerHTML before and after clicking Update (right-click the element in the Elements panel and choose Copy, then Copy outerHTML) and compare the two copies in Text Diff. Like the Elements panel, outerHTML shows attributes, not typed values.
Common mistakes
- Treating View Page Source as the DOM, and writing locators or bug reports from it.
- Checking what a user typed by reading the
valueattribute or the Elements panel; the current value is thevalueproperty. - Assuming that an element a query finds is visible to the user; presence and visibility are separate checks.
- Reading
disabled="false"orhidden="false"as enabled or visible; for these attributes, being present is what counts, whatever the value. - Keeping a reference to an element across a re-render and using it later, which ends in a stale element error.
- Using a locator from "Copy XPath" or from the source without checking in the live DOM that it matches exactly one element.
The QA perspective
On a team, the DOM is common ground between testers and developers. When you report a UI defect, add what the DOM showed: the element (a short selector or its outerHTML), the attribute or property values that matter, and whether the element was missing, hidden or duplicated. "After Update, p.msg keeps its hidden attribute" can be fixed in minutes; "the message does not appear" sends a developer searching.
When you automate, ask developers early for stable hooks — meaningful ids, accessible names or an agreed test id attribute — and ask which parts of the page are re-rendered when data changes. Before you report a failed UI test as a product defect, look at the DOM in the state where it failed: many failures are test problems, such as a stale element, a locator copied from the source or a check made before the content arrived, and the DOM tells you which.
Practice exercise
Read the DOM of a sign-up form
Below is part of a newsletter sign-up page. The requirement: Join stays disabled until the terms are accepted, and after Join is clicked the message "Check your inbox" is shown. A user types ana@example.com and ticks the checkbox. The script at the end runs each time the checkbox changes and sets the button’s disabled property to the opposite of the checkbox’s checked property (! means "not").
Answer the steps on paper first. Then check your answers: save the code as an .html file, open it in a browser and use the Elements panel and the Console.
<section id="signup">
<h2>Newsletter</h2>
<!-- shown to visitors who are not signed in -->
<label for="email">Email</label>
<input id="email" type="email" value="">
<label><input id="terms" type="checkbox"> I accept the terms</label>
<button id="join" disabled>Join</button>
<p id="status" hidden></p>
</section>
<script>
const terms = document.querySelector('#terms');
const join = document.querySelector('#join');
terms.addEventListener('change', () => { join.disabled = !terms.checked; });
</script>- Draw the DOM tree of the
section: element nodes, text nodes and the comment. Leave out whitespace-only text nodes. - After the user’s actions, give the email field’s
valueattribute andvalueproperty, and the checkbox’scheckedattribute andcheckedproperty. - What happens to the button’s
disabledattribute when the box is ticked, and what does the Elements panel show? - Which of these locators match exactly one element:
#signup label,#signup input,#terms,label[for=email]? - A colleague’s test finds
#statusafter clicking Join and passes because the element exists. What is wrong with the check, and what would a better one find?
Show the model answer
Tree: section#signup has six element children in order — h2, label, input#email, label, button#join, p#status — with the comment node between the h2 and the first label. The h2 holds the text node "Newsletter"; the first label holds "Email"; the second label has two children, the checkbox input#terms and the text node " I accept the terms"; the button holds "Join"; p#status has no children. Attributes such as for, type, disabled and hidden sit on their elements, not in the tree.
Email field: the value attribute is still an empty string, as written; the value property is "ana@example.com". Checkbox: there is no checked attribute (getAttribute('checked') returns null), while the checked property is true.
The script sets the disabled property to false; because disabled is reflected, the browser removes the attribute and the Elements panel shows <button id="join">Join</button>. Unticking the box adds disabled back.
#terms and label[for=email] match one element each. #signup label matches two labels, and #signup input matches two inputs: the email field and the checkbox.
The paragraph is in the DOM from the start, hidden and empty, so "it exists" is true before and after the click and proves nothing. A better check asserts that #status is visible and contains "Check your inbox". On this page it would fail: no script fills or shows the message, which is a defect to report, with the element and its hidden attribute as evidence.
Frequently asked questions
Is the DOM the same as the HTML?
No. HTML is the text the server sends; the DOM is the tree the browser builds from it and keeps changing. The parser completes and corrects the markup, and scripts add, remove and change nodes, so after the page loads the two can differ a lot. View Page Source shows the HTML; the Elements panel shows the DOM.
Do testers need to learn the DOM API?
You need to read the DOM, not program it: the node types and their relationships, the difference between attributes and properties, and a few Console commands such as $0 and document.querySelectorAll to check a value or a locator. Automation uses more of the API, but its locators and assertions rest on the same ideas.
Why can my test not find an element I can see on the screen?
Usually because the element is inside an iframe, which is a separate document; it is inside a shadow root; it had not been added yet when the test looked; or the locator was written from the page source rather than the live DOM. Some things on screen are not elements at all: text drawn on a canvas, for example, cannot be found by any DOM locator.
References
- HTML Living StandardThe HTML language, including named character references (entities) and the parsing rules browsers follow.
- WebDriver (W3C)The W3C protocol browsers implement for remote control by automation tools such as Selenium (the Level 2 edition is a Working Draft).
- Document Object Model (DOM) — MDNThe DOM: the tree of nodes a browser builds from a page, which scripts — and UI test tools — read and change.
- Locators — PlaywrightHow Playwright finds elements, why locators re-resolve and auto-wait, and why role-based locators are recommended first.
Practise with Mutqan tools
Beautify HTML with proper indentation of nested elements, preserving pre, script and style blocks.
Compare two texts line by line or word by word and highlight differences.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.