TechByteByByte

Part 1: Web Fundamentals

Understand browsers, HTML, CSS, JavaScript and the web platform behind automated tests.

A browser shows the page, while a server performs hidden work and sends data back.

Think of the browser as a restaurant table, a request as an order, and the server as the kitchen.

click → browser request → server work → response → page changes

Series: Playwright Zero to Expert | Demo app used throughout this series: saucedemo.com

Everything Playwright does eventually comes down to one core skill: finding an element on a page and doing something to it. But “finding an element” is meaningless until you understand what a page actually is, structurally — what HTML gives you, how CSS labels things, and how the browser turns all of it into a live, interactive structure called the DOM. This part builds that vocabulary properly, so that when we reach Part 7 (Locators) and Playwright starts asking you to write things like page.getByRole('button', { name: 'Login' }), none of it feels like magic.


Module 1.1 — HTML

Open any webpage and, underneath everything you see — the buttons, the text, the images — there’s a document written in HTML (HyperText Markup Language). HTML isn’t a programming language in the sense of having logic, loops, or calculations. It’s a markup language: its whole job is to describe the structure and content of a page — “this text is a heading,” “this is a button,” “this is an input field where a user types their email.”

Think of HTML like the skeleton and labels of a building’s blueprint. The blueprint doesn’t decide what color the walls are (that’s CSS’s job, coming up next) or what happens when someone presses a button (that’s JavaScript’s job) — it just says “there’s a door here, a window there, a staircase here.” HTML says “there’s a heading here, a paragraph there, a button here.”

An HTML document is built out of elements. An element is typically written using a pair of tags — an opening tag and a closing tag — with content in between:

<h1>Products</h1>
<p>Browse our latest products below.</p>
<button>Add to cart</button>

Here, <h1> and </h1> are the opening and closing tags of a heading element, <p>/</p> wrap a paragraph, and <button>/</button> wrap a button. The word inside the angle brackets (h1, p, button) is the tag name, and it tells the browser — and, later, Playwright — what kind of element this is.

Elements can carry extra information through attributes, written inside the opening tag:

<input type="text" id="user-name" placeholder="Username" data-test="username" />

This single line already tells you a lot once you know how to read it: it’s an input element (a field a user can type into), its type attribute says it accepts plain text, it has an id of user-name, a placeholder attribute showing greyed-out hint text before the user types, and a custom data-test attribute — which, as you’ll discover very soon in Part 7, is often the single most reliable way to locate an element in automated testing, because unlike CSS classes or visible text, it’s added specifically for testing and rarely changes just because a designer tweaks the styling.

SauceDemo’s login page is a genuinely good real example to study, because its structure follows this exact pattern — roughly:

<div class="login_wrapper">
  <div class="login_logo">Swag Labs</div>

  <input
    type="text"
    class="input_error form_input"
    id="user-name"
    placeholder="Username"
    data-test="username"
  />

  <input
    type="password"
    class="input_error form_input"
    id="password"
    placeholder="Password"
    data-test="password"
  />

  <input
    type="submit"
    class="submit-button btn_action"
    id="login-button"
    value="Login"
    data-test="login-button"
  />
</div>

Even before you know anything about Playwright, you can already read this and understand exactly what a user would do here: type into a username field, type into a password field, click a login button. That’s the entire point of HTML being structured rather than just being a blob of visible text — it exposes meaning, not just appearance, and that exposed meaning is precisely what automation tools latch onto.

A few element types you’ll run into constantly while testing real applications, worth knowing by name and purpose:

  • Forms (<form>) — a container grouping related input fields together, typically submitted as a single unit (like a login form or a checkout form).
  • Inputs (<input>) — fields a user can type into or interact with; the type attribute changes its behavior entirely (text, password, checkbox, radio, submit, email, file, and more).
  • Buttons (<button>, or <input type="submit">) — clickable elements that trigger an action.
  • Links (<a>) — clickable text or elements that navigate somewhere, using an href attribute for the destination.
  • Tables (<table>, with <tr> for rows and <td>/<th> for cells) — used for tabular data, and a genuinely common source of tricky automation scenarios once the data becomes dynamic (Part 11 dedicates real time to this).
  • id — a unique identifier for one specific element on the page. No two elements on a well-built page should share the same id.
  • class — a label that can be shared across many elements, almost always used to apply the same CSS styling (or, sometimes, to group elements for JavaScript behavior) to several elements at once.

This id versus class distinction feels like a small technical detail right now, but it becomes directly important the moment you start writing locators, because it changes how confidently you can target one specific element versus a whole group of similar-looking ones — an id, being unique, is usually a far more precise and stable way to find one exact element than a class, which might be shared by twenty different buttons on the same page.

One habit worth building early: don’t just read HTML from a tutorial — go look at real HTML. Right-click almost anything on a real website and choose “Inspect” and you’ll see the actual HTML behind it. We’ll do this properly and deliberately in Module 1.4.


Module 1.2 — CSS

If HTML is the blueprint’s structure — “there’s a button here” — CSS (Cascading Style Sheets) is what decides how that button actually looks: its color, size, spacing, font, whether it’s visible at all. CSS doesn’t describe structure; it describes appearance, applied onto the structure HTML already defined.

Here’s a small but genuinely important idea: CSS needs a way to say “apply this specific style to that specific element (or group of elements),” and the mechanism it uses to point at elements is called a selector.

This matters to you enormously, because — and this is the connection that makes CSS worth learning properly for a QA engineer, rather than skipping it as “a designer’s problem” — Playwright can use those exact same CSS selectors to find elements for automation. You’re not learning CSS to become a designer; you’re learning it because its addressing system doubles as one of Playwright’s locator strategies.

A basic CSS rule looks like this:

.btn_action {
  background-color: green;
  color: white;
  padding: 10px 20px;
}

.btn_action here is the selector — it says “find every element with the class btn_action, and apply these styles to all of them.” The most common types of selectors you’ll encounter:

  • Type selector — targets every element of a given tag, e.g. button { ... } targets every <button> on the page.
  • Class selector — starts with a dot, targets every element carrying that class, e.g. .btn_action.
  • ID selector — starts with a hash, targets the one element with that id, e.g. #login-button.
  • Attribute selector — targets elements based on an attribute’s value, e.g. [data-test="username"] targets whatever element has exactly that data-test attribute — this one is going to matter a great deal to you specifically, since data-test attributes are common precisely because they’re designed for automation.
  • Descendant combinator (a space).login_wrapper input means “find any <input> element that sits anywhere inside an element with class login_wrapper” — useful for scoping a search to one section of a page rather than the entire document.
  • Child combinator (>).login_wrapper > input is stricter than the descendant combinator: it only matches an <input> that is a direct child of .login_wrapper, not nested several levels deeper inside it.

Take SauceDemo’s login button from Module 1.1 again:

<input
  type="submit"
  class="submit-button btn_action"
  id="login-button"
  data-test="login-button"
/>

There are already at least four completely valid CSS selectors you could write to find this exact element: #login-button (by id), .submit-button (by class), [data-test="login-button"] (by attribute), or even input[type="submit"] (by tag and attribute together, though this one is riskier — what if a second submit button gets added to the page later?).

This is your first real taste of a decision QA engineers make constantly: several selectors might technically work today, but they are not equally safe choices for tomorrow — a theme we’ll return to properly once we reach locator strategy in Part 7.

Hierarchy

is the last core CSS idea worth understanding here: HTML elements nest inside each other, forming layers — a <div> containing a <form> containing several <input> elements, for instance. CSS selectors can express that nesting directly (as you saw with the descendant and child combinators above), and this ability to say “find this element, but only inside that specific section” is exactly what lets you write precise, unambiguous selectors on real, often messy, production pages where the same-looking button might legitimately appear in five different places.


Module 1.3 — The DOM

Here’s something that surprises a lot of beginners: the HTML file a server sends to your browser and the actual, live page your browser is showing you at any given moment are not always the same thing. What the server sent is static text. What your browser builds from it — and keeps updating in real time as JavaScript runs, as you type, as data loads, is called the DOM (Document Object Model).

Analogy: The Restaurant Menu vs. The Kitchen Whiteboard Imagine you walk into a restaurant:

  • The printed paper menu you are handed is the HTML. It is static, printed at a factory, and cannot change unless the waiter brings a completely new piece of paper.
  • The kitchen’s live dry-erase whiteboard is the DOM. As ingredients run out or daily specials are created, the head chef (JavaScript) writes or erases items on the board. The customers in the dining room see these changes immediately, even though the paper menus at their tables still look exactly the same.

How the Browser Parses HTML into DOM Nodes

When a browser loads an HTML file, it processes the text character-by-character:

  1. Tokenization: It identifies tags (like <div> or <input>), attributes (like id or class), and text content.
  2. Node Creation: Every tag is instantiated as a unique programming object (a “Node”) containing properties representing its attributes and styling.
  3. Tree Assembly: It nests these nodes according to the HTML tags’ parent-child hierarchy, creating a live data structure in memory.

📊 Visual Flowchart: HTML-to-DOM Rendering Pipeline

Here is how the browser compiles static markup into a dynamic tree structure:

graph TD
    HTML["Raw HTML Text<br>&lt;div class='login_wrapper'&gt;&lt;input id='user-name'&gt;&lt;/div&gt;"] --> Parser["Browser HTML Parser"]
    Parser --> Tokenizer["Tokenize Tags, Classes, & IDs"]
    Tokenizer --> Nodes["Instantiate Node Objects in Memory"]
    Nodes --> DOMTree["Assemble DOM Tree Hierarchy"]
    DOMTree --> View["Render Visual Screen Layout"]

JS["JavaScript execution<br>(e.g. User clicks Add to Cart)"] -.->|Mutate Nodes| DOMTree

Think of the DOM as a living, breathing tree structure that the browser constructs the instant it reads your HTML. Every HTML element becomes a “node” in this tree, connected to the elements around it:

                    <html>

              ┌────────┴────────┐
            <head>            <body>

                      ┌──────────┴──────────┐
                  <div class="login_wrapper">  ...

              ┌─────────┼─────────┐
          <input>    <input>   <input>
        (username)  (password) (login button)
  • The <html> element is the parent of <head> and <body>.
  • <head> and <body> are siblings — they share the same parent.
  • The three <input> elements inside .login_wrapper are its children, and siblings of each other.

Why does this tree structure matter so much for you specifically? Two reasons, and both come up constantly once you’re writing real Playwright tests.

First, the DOM changes after the page loads, and Playwright is watching the DOM, not the original HTML file. When you click “Add to Cart” on SauceDemo, JavaScript running in the browser reaches into the DOM tree and adds a new element — the cart badge showing “1” — without ever reloading the page or fetching a new HTML file from the server.

If you only ever looked at the original HTML the server sent, you’d never see that badge; it doesn’t exist there. It only exists in the live DOM, built and modified by JavaScript, after the fact.

This is precisely why Playwright interacts with a live browser instead of just downloading and parsing raw HTML text — a huge amount of what you’ll test is content and behavior that simply doesn’t exist until JavaScript creates it.

Second, that parent/child/sibling relationship is exactly what CSS combinators and, later, XPath axes are built to navigate. When you write .login_wrapper input, you are literally asking Playwright to walk the DOM tree, starting from the element with class login_wrapper, and find every <input> descendant beneath it. Understanding the tree shape makes selectors stop feeling like arbitrary syntax and start feeling like simple, logical directions — “go here, then look inside that.”

A genuinely common point of confusion for beginners: “View Page Source” (right-click → View Page Source, or Ctrl+U) shows you the original HTML the server sent — static, unchanging, exactly as it arrived. The Elements tab in DevTools (coming up next in Module 1.4), by contrast, shows you the live DOM — current, up-to-the-second, including everything JavaScript has added, removed, or changed since the page loaded.

If you’re ever debugging something and the HTML you’re staring at doesn’t seem to match what’s actually on screen, check which of these two you’re actually looking at — it’s one of the single most common beginner mix-ups, and it costs people real time until they learn to notice it.


Module 1.4 — Browser DevTools

DevTools is the single most important non-code tool you will use throughout this entire series — more than any Playwright API, more than any code editor feature. It’s the window that lets you see everything we’ve talked about so far — HTML, the live DOM, CSS, and, soon, the network requests from Part 0 — instead of just imagining it.

Every modern browser (Chrome, Firefox, Edge) ships DevTools for free, built in. Open SauceDemo right now and press F12 (or right-click anywhere on the page and choose “Inspect”) to bring it up as you read this.

  • Elements tab — shows you the live DOM tree from Module 1.3, exactly as the browser currently sees it. Hover over any line here and the browser highlights the corresponding element directly on the page — an incredibly fast way to connect “this HTML” to “that visible thing.” This is also where you’ll spend the most time while writing Playwright locators later, since it lets you inspect an element’s tag, attributes, classes, and id directly. Try it now: right-click SauceDemo’s “Login” button and choose “Inspect” — you’ll land directly on its line in the Elements tab.
  • Console tab — a place where JavaScript can be run directly, and where the page itself often logs error messages when something goes wrong. If a page is silently broken, the Console is frequently the first place a red error message will explain why.
  • Network tab — the tab we already leaned on conceptually back in Part 0: every request the page makes (HTML, CSS, JS, images, and API calls) shows up here, along with its response, status code, and timing. Reload SauceDemo with this tab open and watch requests populate in real time — this is exactly what we asked you to try as an exercise in Part 0, now with the vocabulary to actually understand what you’re seeing.
  • Sources tab — shows the actual JavaScript files the page is running, and lets you set breakpoints to pause code mid-execution. You won’t need this constantly early on, but it becomes genuinely useful once you’re debugging complex frontend behavior later in the series.
  • Application tab — this is where a browser stores data about your visit that isn’t part of the page’s HTML at all:
    • Cookies — small pieces of data a website stores in your browser, often used to remember that you’re logged in.
    • Local Storage — a simple key-value storage area a website can use to persist data in your browser, that stays even after you close the tab or browser.
    • Session Storage — similar to Local Storage, but cleared the moment you close the tab.

These three matter directly for QA work well beyond casual browsing — Part 18 (Authentication) is built almost entirely around understanding and manipulating exactly these mechanisms, since “staying logged in” is usually implemented using one of them.

A small, deliberate exercise worth doing right now, before moving on: open SauceDemo, log in with standard_user / secret_sauce, and open Application → Local Storage. You’ll likely see data appear there the moment you log in — proof, directly in front of you, that the application is storing something about your session in the browser itself, not just relying on the server to remember you.


Module 1.5 — CSS Selectors, in Depth

We introduced CSS selectors briefly in Module 1.2 as a styling mechanism. Now let’s treat them properly as what they’ll actually become for you: one of Playwright’s core locator strategies (formally covered in Part 7, but the syntax itself belongs here, alongside the rest of web fundamentals).

Beyond the type, class, id, and attribute selectors already covered, a few more patterns come up constantly in real testing work:

  • Combining selectorsinput.form_input means “an <input> element that also has the class form_input” — narrower than either condition alone.
  • Multiple attribute conditionsinput[type="submit"][data-test="login-button"] requires both conditions to be true on the same element.
  • :nth-child() — targets an element based on its position among its siblings, e.g. tr:nth-child(2) targets the second row in a table. Useful, but worth treating cautiously — a row’s position can shift the moment sort order or filtering changes, which makes position-based selectors one of the more fragile choices available to you (this exact caution reappears, deliberately, in Part 7).
  • :hover, :focus, :checked — pseudo-classes representing an element’s current state rather than its static structure — a checkbox that is currently checked matches :checked, for instance.

Here’s a concrete, realistic exercise using SauceDemo’s structure from Module 1.1. Given:

<input
  type="text"
  class="input_error form_input"
  id="user-name"
  placeholder="Username"
  data-test="username"
/>

All of the following CSS selectors correctly find this exact field:

#user-name
.form_input
[data-test="username"]
input[placeholder="Username"]
input.input_error.form_input

They all work — but they are not equally good choices, and this is worth sitting with rather than rushing past. #user-name and [data-test="username"] are both strong choices, because an id and a purpose-built test attribute are unlikely to change just because a designer edits the visual styling. .form_input, by contrast, is risky — it’s very plausible that many input fields across the whole site share that exact class for styling consistency, meaning this selector might not even point at a single, unique element once used elsewhere on the page.

This gap — between “technically works right now” and “will still reliably work after the next unrelated design change” — is the single most important judgment call in all of locator strategy, and you’ll see it again explicitly once we build real Playwright locators in Part 7.


Module 1.6 — XPath

XPath (XML Path Language)

is a different way of expressing “find this element,” built originally for navigating XML documents, but fully usable on HTML too, since HTML is structurally similar enough. Where CSS selectors describe an element mostly by its attributes and class relationships, XPath describes an element by literally walking the tree — “start here, go into this child, then that sibling” — and, notably, it can also search based on an element’s actual visible text, something CSS selectors alone cannot do at all.

A basic XPath expression to find SauceDemo’s login button by its visible text might look like:

//input[@value="Login"]

Read piece by piece: // means “search anywhere in the document,” input means “look for an <input> element,” and [@value="Login"] means “whose value attribute equals Login.” This is doing something a plain CSS selector genuinely cannot do on its own — CSS has no built-in way to say “find the element whose visible text says X.” XPath’s ability to search by text is exactly why it hasn’t disappeared even as CSS-based and role-based locators have become the more commonly recommended default — sometimes text really is the most natural, human-readable way to identify something, especially buttons and links.

A few more XPath patterns worth recognizing:

  • contains()//button[contains(text(), "Add to cart")] finds a button whose text contains that phrase, even if there’s more text around it (useful when the exact full text is long, dynamic, or slightly unpredictable).
  • Navigating relationships directly//div[@class="login_wrapper"]/input[1] walks from a specific div, down into its first input child, expressed as a literal path, step by step.
  • Absolute vs. relative paths — an XPath starting with a single / (like /html/body/div/input) is an absolute path, describing the element’s exact position from the very root of the document. This is extremely fragile in real testing — insert one new <div> anywhere above that input and the entire path breaks. A path starting with // is relative — it searches anywhere in the document matching the given pattern, which is far more resilient to unrelated structural changes elsewhere on the page.

It’s worth being upfront and honest about something here, because it’s a genuine, common debate in the QA world, and interviewers like probing candidates’ actual opinions on it: XPath is powerful — genuinely more powerful than CSS selectors in raw capability, especially for text-based searches and complex tree navigation — but it also tends to produce longer, harder-to-read, and more brittle expressions when people lean on absolute paths or overly deep tree-walking instead of simple, targeted patterns.

Modern Playwright locator strategy (which you’ll meet properly in Part 7) actively encourages reaching for role-based and text-based locators first, CSS selectors and data-test attributes second, and XPath only when nothing else reasonably solves the problem — not because XPath is “wrong,” but because the simpler options tend to survive UI changes better, and readability matters enormously once a test suite has hundreds of tests maintained by more than one person.

A mistake that shows up constantly among people newer to automation — often because early tutorials, or auto-generated locators from browser recording tools, lean on it heavily — is defaulting to XPath, and specifically absolute XPath, for everything, simply because it always technically works on the page in front of them right now. It works today.

The real question — the one that separates a maintainable suite from a constantly-breaking one — is whether it will still work after the next unrelated change to the page, and that question is exactly what the rest of this series, especially Part 7 and Part 39, is going to keep training you to ask automatically.


How It Works in a Real Test Run

When Playwright opens a page, the browser requests resources, parses HTML into the DOM, applies CSS, executes JavaScript, and may request more data. A locator searches the current browser representation, not the original HTML file stored on the server.

This explains many failures: an element may exist in source HTML but be hidden by CSS, appear later through JavaScript, live inside an iframe, or have an accessible role different from its tag name.

Interview Questions

Q: What is the difference between HTML and CSS?

Ans: HTML describes the structure and content of a page — what elements exist and what they contain, like headings, inputs, and buttons. CSS describes how those elements should visually appear — color, spacing, size, layout. HTML is the blueprint’s structure; CSS is what decides how that structure actually looks once built.

Q: What is the difference between an id and a class in HTML, and why does that distinction matter for testing?

Ans: An id is meant to be unique to exactly one element on a page, while a class can be shared across many elements, typically for consistent styling. This matters for testing because an id generally lets you target one specific element with confidence, while a class might match many elements at once, making it a riskier, less precise way to locate exactly the element you intend to interact with.

Q: What is the DOM, and how is it different from the raw HTML a server sends?

Ans: The DOM is the live, in-memory tree structure the browser builds from the HTML it receives, and it keeps changing as JavaScript runs, as data loads, and as the user interacts with the page. The raw HTML a server sends is static — a snapshot in time. Content added dynamically by JavaScript after the page loads exists in the DOM but never existed in, and won’t appear in, the original HTML document. This is why “View Page Source” and the DevTools Elements tab can show two different things.

Q: Why does Playwright interact with the live DOM rather than the original HTML source?

Ans: Because a great deal of real, testable content and behavior — cart counters updating, dynamic tables, elements that appear only after a button is clicked — is created entirely by JavaScript after the page has already loaded, and simply doesn’t exist in the static HTML the server originally sent. A tool that only read the raw HTML would be blind to almost everything an interactive web application actually does.

Q: Given the same HTML element, several different CSS selectors might all technically find it. How would you decide which one to actually use?

Ans: I’d prioritize selectors that are unlikely to change for reasons unrelated to the element’s actual purpose — an id or a dedicated data-test attribute, since those tend to be stable across styling and layout changes. I’d be far more cautious about selecting by a shared CSS class, since the same class is often reused across many elements for styling consistency and may not even uniquely identify the element I actually want, and it’s especially likely to change if a designer restyles the page. The guiding question I’d ask isn’t “does this work right now,” but “is this likely to still work after the next unrelated UI change.”

Q: What can XPath do that a plain CSS selector cannot?

Ans: XPath can select elements based on their visible text content, using expressions like contains(text(), "..."), which CSS selectors have no native way to express at all. XPath can also express more complex tree relationships, like navigating to a sibling or parent, in ways CSS’s combinators don’t fully support.

Q: What’s the difference between an absolute and a relative XPath, and why does it matter?

Ans: An absolute XPath starts from the root of the document and describes an element’s exact position step by step, e.g. /html/body/div/input. A relative XPath, starting with //, searches anywhere in the document for a matching pattern instead of depending on exact position. Absolute XPaths are fragile — inserting a single new element anywhere above the target in the tree breaks the entire path — while relative XPaths are far more resilient to unrelated structural changes, which is why relative XPath is almost always the better choice in real automation.

Q: A generated locator (from a recording tool, for instance) uses a long absolute XPath and it technically works. Would you keep it as-is? Why or why not?

Ans: I generally wouldn’t keep it as-is. The fact that it works right now doesn’t say much about whether it’ll keep working — an absolute XPath breaks the moment anything changes in the page’s structure above the target element, even something completely unrelated to that element itself, like a banner being added higher up the page. I’d replace it with a more targeted, stable option — ideally a data-test attribute or id, or a relative XPath/CSS selector scoped tightly to the element I actually care about — because long-term test stability matters more than a locator simply working in this one moment.


Exercises — Part 1

Understand: Open SauceDemo, right-click the “Login” button, and choose Inspect. Identify: its tag name, its id (if any), its class (if any), and any data-test attribute. Write all four down.

Simple Practice: Still in the Elements tab, find SauceDemo’s username input field. Write three different CSS selectors that would all correctly match it, using the patterns from Module 1.5 (id selector, attribute selector, and one combined selector). For each, note whether you think it’s a safe long-term choice or a risky one, and why.

Real-World Scenario: Log into SauceDemo, add two different products to the cart, and open the Elements tab. Find the cart icon showing the item count. Was this element present in the page before you clicked “Add to Cart,” or did it appear only afterward? Then open “View Page Source” (Ctrl+U) and search for that same cart count text — is it there? What does that tell you about the difference between the DOM and the raw HTML source, using your own words?

Challenge: Pick any real product page on any e-commerce site you like. Using DevTools’ Elements tab, find the “Add to Cart” (or equivalent) button, and write down: one CSS selector you’d trust for long-term automation, one CSS selector you would deliberately avoid and why, and one relative XPath expression that would also correctly find it.


Next: Part 2 — JavaScript Fundamentals for QA

— now that you can read a page’s structure, it’s time to learn the language Playwright itself is written in and controlled with, taught only to the depth a QA engineer actually needs.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed