When you start writing test automation with Selenium or Playwright, a common frustration is “I can’t find the element” or “I have no idea what locator to use.” In most cases, the root cause is simply not knowing how to read HTML. This guide covers the HTML fundamentals every QA engineer needs for test automation — with real code examples throughout.
💡 Term: What Is a “Locator”? A locator is any method used to identify an element on a page in Selenium or Playwright. CSS selectors and XPath are both types of locators. You may also see the term “selector” used interchangeably — in this article, we’ll use “locator” as the standard term. |
📌 Who This Article Is For
| ✅ Beginners with Selenium or Playwright who have little to no experience reading HTML |
| ✅ Engineers unsure what to look for when writing locators |
| ✅ Anyone who opens DevTools and sees HTML but doesn’t know where to start |
| ✅ QA engineers with no web development background who want to get into test automation |
✅ What You’ll Learn
|
👨💻 About the Author Written by a QA engineer with 15+ years of hands-on experience in test automation using Selenium and Playwright. This guide is based on real-world experience — including the frustration of not being able to write a single locator because HTML felt like a foreign language. |
📌 Key Takeaways
|
Every test automation script involves one thing above all: finding elements. In Selenium, that looks like driver.find_element(By.ID, "email"). In Playwright, it might be page.get_by_test_id("login-btn"). But where do the values "email" and "login-btn" actually come from?
The answer is the HTML. Test automation tools find elements using the attributes defined in HTML. That’s why being able to read HTML is one of the most direct shortcuts to becoming faster at test automation — and you don’t need a web development background to do it.
HTML Structure: What Are Tags and Attributes?
HTML is the language that defines the structure of a web page. When a test automation tool “clicks a button” or “types into an input field,” it finds those elements using the attributes written in HTML. For a guide on how to inspect those attributes in practice, see Chrome DevTools for QA Engineers.
The Basics of Tags
HTML is made up of tags (written as <tagname>). Most tags come in pairs — an opening tag and a closing tag.
<button>Log In</button>
<p>This is a paragraph of text.</p>
<input type="text">What Are Attributes?
Tags can carry attributes — additional information written inside the opening tag in the format attribute-name="value".
<input type="email" id="user-email" name="email" placeholder="Email address">
These attribute values are exactly what you use as the basis for writing locators in your test code.
5 Attributes That Matter Most for Test Automation
HTML has dozens of attributes, but test automation relies on just a handful. Master these five and you can write locators for nearly any element.
| Attribute | Role | How It’s Used in Locators | Stability |
|---|---|---|---|
id | Unique identifier on the page | By.ID / #email | ⭐⭐⭐ Top priority |
data-testid | Test-only identifier | [data-testid='xxx'] | ⭐⭐⭐ Top priority |
name | Form field key sent on submit | By.NAME / [name='email'] | ⭐⭐ Relatively stable |
class | Style group name | By.CLASS_NAME / .btn-primary | ⭐ Breaks on design changes |
type | Input field type | [type='submit'] | ⭐⭐ Useful when purpose is clear |
① id: The Most Reliable Identifier
The id attribute must be unique within the page — only one element can have a given id at a time. That uniqueness makes it the most stable locator.
<input id="login-email" type="email">
<button id="login-btn">Log In</button># Selenium
driver.find_element(By.ID, "login-email")
driver.find_element(By.CSS_SELECTOR, "#login-btn")
# Playwright
page.locator("#login-email")
page.locator("#login-btn")② data-testid: The Gold Standard for Test Locators
data-testid is an attribute that developers add specifically for testing purposes. Because it’s not tied to styling or business logic, it’s resilient to design changes and refactoring — making it the most stable locator option in test automation.
<button data-testid="submit-button" class="btn btn-primary btn-lg">
Submit
</button># Selenium (using CSS selector)
driver.find_element(By.CSS_SELECTOR, "[data-testid='submit-button']")
# Playwright (dedicated method)
page.get_by_test_id("submit-button")💡 What if data-testid isn’t there? Ask the developers: “Could you add a In practice, it’s not uncommon for screens to have no id attributes at all. In those cases, requesting a |
💡 HTML Tags Have Built-In Roles A |
③ class: Convenient, but Fragile
class is a grouping attribute used to apply CSS styles. Multiple elements can share the same class name, which makes it an unreliable choice for locators.
<button class="btn btn-primary btn-lg disabled">Submit</button>
↑ Changes with design updates or state changes
<button class="MuiButton-root css-1abcde">Submit</button> <!-- Material UI -->
<button class="chakra-button">Submit</button> <!-- Chakra UI -->
<button class="Button_btn__abc123">Submit</button> <!-- CSS Modules -->
↑ May change with every build⚠️ Treat class as a Last Resort Classes like |
Form Elements You’ll Use in Almost Every Test
Form elements show up constantly in web app testing. Logins, registrations, searches — nearly every key user flow involves a form.
Form Element to Locator Reference Table
| HTML Tag | What It Renders As | Selenium Example | Playwright Recommended |
|---|---|---|---|
<input type="text"> | Text input | find_element(By.ID, "...").send_keys("value") | get_by_label("Label") |
<input type="email"> | Email input | find_element(By.ID, "...").send_keys("value") | get_by_label("Email") |
<input type="password"> | Password input (masked) | find_element(By.ID, "...").send_keys("value") | get_by_label("Password") |
<input type="checkbox"> | Checkbox | find_element(By.ID, "...").click() | get_by_label("Agree to terms") |
<button> | Button | find_element(By.ID, "...").click() | get_by_role("button", name="Log In") |
<select> | Dropdown | Select(element).select_by_value("val") | select_option("val") |
<textarea> | Multi-line text input | find_element(By.ID, "...").send_keys("value") | get_by_label("Comment") |
<a> | Link | find_element(By.LINK_TEXT, "text") | get_by_role("link", name="text") |
Practice: Reading a Login Form and Writing Locators
Let’s walk through a real HTML example and write locators from it.
<form id="login-form">
<label for="email">Email address</label>
<input type="email"
id="email"
name="email"
data-testid="login-email"
placeholder="e.g. user@example.com">
<label for="password">Password</label>
<input type="password"
id="password"
name="password"
data-testid="login-password">
<button type="submit"
id="login-btn"
data-testid="login-submit">
Log In
</button>
</form>Here’s how you’d write the locators from this HTML:
# Selenium — id first
email_input = driver.find_element(By.ID, "email")
password_input = driver.find_element(By.ID, "password")
login_button = driver.find_element(By.ID, "login-btn")
email_input.send_keys("test@example.com") # sample URL
password_input.send_keys("password123")
login_button.click()
# Playwright option ① — get_by_test_id (data-testid available)
page.get_by_test_id("login-email").fill("test@example.com") # sample URL
page.get_by_test_id("login-password").fill("password123")
page.get_by_test_id("login-submit").click()
# Playwright option ② — semantic locators (get_by_label / get_by_role)
page.get_by_label("Email address").fill("test@example.com") # sample URL
page.get_by_label("Password").fill("password123")
page.get_by_role("button", name="Log In").click()Locator Priority: What to Look for First
Selenium: Prefer Stable Attribute-Based Locators
| ① id | → | ② data-testid | → | ③ name | → | ④ CSS | → | ⑤ XPath |
More stable on the left → more fragile on the right
Playwright: Match the Locator to the Element Type
Playwright’s approach isn’t a fixed ranking — it’s about choosing the right locator for each element type. The right choice depends on context.
| Element Type | Recommended Locator | Example |
|---|---|---|
| Buttons, links, interactive elements | get_by_role() | get_by_role("button", name="Log In") |
| Input fields with a label | get_by_label() | get_by_label("Email address") |
| Elements with data-testid | get_by_test_id() | get_by_test_id("login-submit") |
| When none of the above work well | locator() | locator("#email") |
💡 Playwright’s philosophy: Prefer locators that reflect how a user perceives the element. That said, get_by_test_id() can be more stable in many real-world situations — the right choice depends on what’s available in the HTML. |
Common Pitfalls When Reading HTML
🚧 Watch Out for These ① The id changes on every page load Some frameworks generate dynamic ids like ② Multiple classes — which one to use? Classes like ③ get_by_label() doesn’t work If ④ Multiple elements share the same class If ⑤ The “button” is actually a <div> or <span> A visual button on screen might be |
Frequently Asked Questions
Use Chrome DevTools (F12) → Elements tab. Right-click any element and select “Inspect” to highlight it in the HTML tree. For a full guide, see Chrome DevTools for QA Engineers.
Not for test automation purposes. Knowing tags, attributes, and form elements — combined with the ability to read HTML in DevTools — is more than enough. You don’t need to reach a front-end developer’s level of knowledge.
id must be unique on the page (think of it like a name), while class can be shared by many elements (think of it like a team label). For locators, id is preferred because its uniqueness makes it reliably point to a single element.
Typically frontend developers add it. QA engineers request it: “Could you add a data-testid to this element?” It’s a low-effort change that significantly improves test stability, so most developers are happy to accommodate. Establishing the convention early in a project goes a long way.
No, but having it makes a noticeable difference in test stability. Use id or name when they’re available; when they’re not, requesting a data-testid is the recommended path.
The underlying concept is the same, but the syntax differs. Selenium uses attribute-based methods like By.ID and By.CSS_SELECTOR. Playwright recommends choosing a locator by element type — get_by_role(), get_by_label(), or get_by_test_id() — depending on what’s available. Both tools ultimately rely on HTML attributes as their source of truth.
In Selenium, CSS selectors are the common choice — concise and fast. In Playwright, semantic locators like get_by_role() and get_by_label() are recommended first. Both CSS and XPath serve as fallback options when those don’t apply. For a deep dive, see CSS Selectors & XPath Basics.
Summary
|
You don’t need to memorize all of HTML. Knowing “look for id and data-testid” and “recognize common form elements” is enough to clear the first big obstacle in test automation. Once you’re comfortable reading HTML, the next step is using Chrome DevTools to inspect real pages and validate your selectors on the spot.
🗺️ Your Learning Path: HTML to Test Automation
|
