TechByteByByte

Part 6: Playwright Fundamentals

Start browser automation with Playwright, real browsers and your first working test.

Playwright controls a real browser through code. A browser context is one clean user session, and a page is one tab in it.

Think of a browser as a building, a context as one private apartment, and pages as rooms.

test runner → browser → isolated context → page

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

Everything so far — HTML, CSS, the DOM, JavaScript, TypeScript, Node.js, npm, Git — was preparation. This is where it starts paying off directly. By the end of this part, you’ll have Playwright installed, a real project structure in front of you that you actually understand rather than just accept, and your first test genuinely running against a real browser.


What Is Browser Automation, and Why Does It Exist?

Think back to Module 0.4: manual testing means a human clicking through an application by hand, comparing what they see against what they expect. Now imagine doing that for SauceDemo’s login page — valid user, locked-out user, wrong password, empty username, empty password — across Chrome, Firefox, and Safari, once a day, for months. It’s not that a human can’t do this. It’s that a human doing this repeatedly, precisely, without ever getting careless or bored, simply isn’t realistic at scale.

Browser automation

is software controlling a real browser — opening it, navigating it, clicking things in it, reading what it shows — the same actions a human would take, but performed by code, reliably, at machine speed, as many times as needed. Playwright is a browser automation framework, originally built by Microsoft, that gives you a clean, modern API to do exactly this.

Analogy: The Screenplay, the Director, and the Actor Think of browser automation as a film production set:

  • The Screenplay (Your Test Script): The written screenplay login.spec.ts outlines exactly what movements must happen: “Walk to the counter, enter your name, press the submit button.”
  • The Director (Playwright): Reads the screenplay and coordinates the browser through Playwright’s client, driver, and browser-specific integration. Local and remote setups can use different transports, so the important idea is the ongoing command-and-event channel rather than one universal WebSocket.
  • The Actor (The Browser): Chromium, Firefox, or WebKit listens to the earpiece and physically acts out the movements on the set (the screen) in real-time.

It’s worth being precise about something here, because it removes a common misconception early: Playwright doesn’t simulate a browser, or fake one. When a Playwright test runs, an actual copy of Chromium, Firefox, or WebKit genuinely launches on your machine (or on a CI server), and Playwright sends it real commands, the same way a human’s clicks and keystrokes would. This is exactly why Playwright can find real bugs that only appear in a real, rendering browser — layout issues, JavaScript errors, things that a purely theoretical, non-browser-based check could never catch.


Playwright vs. Selenium vs. Cypress

This comparison comes up constantly in interviews, and it’s worth understanding at more than a surface, buzzword level.

Selenium

is the older, longest-standing tool in this space, and for years was the default choice for browser automation. WebDriver Classic uses standardized command-and-response endpoints, while modern Selenium is also adopting the bidirectional WebDriver BiDi standard for streaming browser events. Selenium can produce reliable suites when tests use sound locators, waits, isolation, and framework design; protocol choice alone does not determine flakiness.

Cypress

came later, built specifically to feel fast and developer-friendly, with excellent debugging tools and a different in-browser execution model. Its capabilities continue to evolve, so compare current documentation for the exact browser, origin, tab, component-testing, and language requirements of your project instead of relying on an old feature checklist.

Playwright

provides one API across supported Chromium, Firefox, and WebKit builds while its driver handles browser-specific integration. Its normal high-fidelity client/server connection uses the Playwright protocol; connectOverCDP() is a separate Chromium-only, lower-fidelity option. Playwright’s integrated locators, actionability checks, contexts, tracing, and test runner are more useful reasons to choose it than the oversimplified claim that “WebSocket automatically makes it faster.”

📊 Visual Flowchart: Playwright Architecture

Here is how Playwright connects directly to isolated browser contexts:

graph TD
    TestRunner["Playwright Test Runner<br>(Node.js Process)"] --> Client["Playwright client and driver"]
    Client --> Integration["Browser-specific integration"]
    Integration --> Browser["Browser Process<br>(Chromium/Firefox/WebKit)"]

Browser --> Context1["Browser Context 1<br>(Isolated Cookies/Storage)"]
    Browser --> Context2["Browser Context 2<br>(Isolated Cookies/Storage)"]

Context1 --> Page1["Page/Tab 1"]
    Context1 --> Page2["Page/Tab 2"]

None of this means Selenium or Cypress are “bad” — Selenium remains extremely widely used in large, established codebases, and Cypress still has real strengths for certain frontend-focused workflows. But for a QA engineer starting fresh today, especially one who wants one consistent tool covering UI, API, and cross-browser testing (recall the testing pyramid discussion from Part 0), Playwright’s design genuinely earns its current popularity — and understanding why, not just that it’s popular, is exactly the kind of answer that stands out in an interview.


Installation and Project Creation

With Node.js installed (Part 4) and a terminal open, create a folder for your project and run:

npm init playwright@latest

This single command does several things at once, and it’s worth watching each prompt rather than blindly hitting enter:

✔ Do you want to use TypeScript or JavaScript? · TypeScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · true
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true
  • TypeScript or JavaScript — this series uses TypeScript, for exactly the reasons covered in Part 3.
  • Where to put your tests — a folder (conventionally tests) where your test files will live.
  • GitHub Actions workflow — generates a starter CI/CD configuration file (Part 32 covers this properly; for now, it’s fine to accept — you don’t need to understand it yet to move forward).
  • Install Playwright browsers — this downloads actual copies of Chromium, Firefox, and WebKit onto your machine. This is worth pausing on: these are not the same Chrome or Firefox you might already have installed for everyday browsing. Playwright manages its own dedicated browser binaries, specifically version-matched to the Playwright version you’re using, so that your tests behave identically regardless of what browsers happen to already be installed on whatever machine is running them — including a CI server that may have no browsers at all otherwise.

Once this finishes, you have a real, runnable Playwright project.


Project Structure

A fresh Playwright project looks roughly like this:

my-playwright-project/
├── tests/
│   └── example.spec.ts
├── tests-examples/
│   └── demo-todo-app.spec.ts
├── playwright.config.ts
├── package.json
├── package-lock.json
└── node_modules/
  • tests/ — where your actual test files live. Playwright, by convention, looks for files ending in .spec.ts (or .spec.js) here.
  • tests-examples/ — a folder of sample tests Playwright generates to show you patterns; genuinely useful to skim once, but not something you’ll build on directly — feel free to delete it once you’re comfortable.
  • playwright.config.ts — the project’s central configuration file, and important enough to deserve its own full section next.
  • package.json, package-lock.json, node_modules/ — exactly what you learned in Part 4, with @playwright/test now listed as a devDependency.

playwright.config.ts, Explained Line by Line

This file controls how your entire test suite behaves — which browsers it runs against, how long it waits before giving up, what gets recorded when a test fails, and much more. A trimmed, realistic starting version looks like this:

import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",

  fullyParallel: true,

  retries: 0,

  reporter: "html",

  use: {
    baseURL: "https://www.saucedemo.com",
    trace: "on-first-retry",
  },

  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
  ],
});

Let’s go through this properly, because glossing over configuration is exactly how people end up with a project full of settings nobody actually understands or dares to change.

  • defineConfig({...}) — a helper function Playwright provides that gives you TypeScript auto-completion and type-checking on every setting inside it, so a typo in a setting name gets caught immediately, in your editor, rather than silently ignored.
  • testDir: './tests' — tells Playwright where to look for test files. If your tests aren’t found when you run npx playwright test, this is one of the very first places to check.
  • fullyParallel: true — allows tests to run at the same time, across multiple workers, rather than strictly one after another. This is a direct preview of Part 27; for now, know that it’s a major reason Playwright suites can run so much faster than older, purely sequential ones.
  • retries: 0 — how many times a failing test automatically re-runs before being reported as truly failed. It’s deliberately set to 0 in a fresh project, and it’s worth understanding why this default matters, ahead of a full discussion in Part 28: retries can hide real flakiness rather than fix it, so relying on a high retry count as your default strategy for a shaky suite is treating a symptom, not the actual problem.
  • reporter: 'html' — after a test run finishes, generates a visual HTML report you can open in a browser, showing exactly what passed, what failed, and why (screenshots, traces, and more — Part 34 covers reporting properly).
  • use: { baseURL: ... } — sets a base URL every test can build on. With this set, a test can write page.goto('/inventory.html') instead of the full page.goto('https://www.saucedemo.com/inventory.html') every single time — small, but it means your entire suite can be pointed at a different environment (staging vs. production, covered properly in Part 16) just by changing this one line, rather than editing every test file.
  • use: { trace: 'on-first-retry' } — controls when Playwright records a detailed, replayable trace of a test’s execution (Part 24 dives deep into this) — here, only the first time a test is retried after failing, balancing genuinely useful debugging information against not recording (and storing) a trace for every single test run unconditionally.
  • projects: [...] — defines which browser(s), and which configurations, your tests actually run against. A single chromium project here means tests run only in Chromium for now; Part 26 shows you how to add Firefox, WebKit, and even mobile device emulation as additional projects, all from this same array.

None of these settings are arbitrary. Every one exists to answer a real, specific question a testing framework has to answer somehow — and once you can read this file and explain why each line exists, you’ve moved from “copying a config file” to genuinely configuring a framework.


Your First Test

Let’s write a real test against SauceDemo, in tests/login.spec.ts:

import { test, expect } from "@playwright/test";

test("user can login with valid credentials", async ({ page }) => {
  // Navigate to the login page (baseURL from the config makes this a relative path)
  await page.goto("/");

  // Locate the username field and type into it
  await page.getByPlaceholder("Username").fill("standard_user");

  // Locate the password field and type into it
  await page.getByPlaceholder("Password").fill("secret_sauce");

  // Locate the Login button and click it
  await page.getByRole("button", { name: "Login" }).click();

  // Assert that the inventory page's heading is now visible,
  // confirming the login actually succeeded
  await expect(page.getByText("Products")).toBeVisible();
});

You already have every piece of vocabulary needed to read this properly, from Part 2 and Part 0: test(...) is a function call taking a description and an async arrow function; { page } destructures Playwright’s provided page tool; every await exists because the action or assertion on that line genuinely takes real time to complete.

The only genuinely new pieces here are page.getByPlaceholder(...), page.getByRole(...), and expect(...) — Playwright’s actual locator and assertion methods, which get a full, dedicated treatment starting in Part 7 and Part 9. For now, read them exactly as they sound: “find the element with this placeholder text,” “find the button with this accessible name,” “assert that this thing is true.”


Running Tests

npx playwright test
Running 1 test using 1 worker

✓  1 tests/login.spec.ts:3:1 › user can login with valid credentials (1.2s)

1 passed (1.4s)

By default, Playwright runs headless — the browser genuinely launches and does everything described in your test, but without a visible window, since nothing about the test actually requires a human to watch it happen, and headless execution is meaningfully faster. This is also exactly how tests run on a CI server (Part 32), which typically has no display at all.

To watch the browser while a test runs — genuinely valuable while you’re learning, or while debugging something confusing — run in headed mode instead:

npx playwright test --headed

To view the HTML report generated after a run (recall reporter: 'html' from the config above):

npx playwright show-report

This opens an interactive report in your browser — every test, its pass/fail status, timing, and (for failures) screenshots and traces, all in one place. You’ll come to rely on this report constantly, especially once test suites grow large enough that scrolling through raw terminal output stops being practical.

UI Mode — Genuinely Worth Learning Early

Playwright also ships something called UI Mode, which is worth adopting early rather than treating as an advanced feature to discover later:

npx playwright test --ui

This opens an interactive window showing every test in your suite, letting you run them individually, watch each step of a test execute with a visual timeline, inspect the DOM at any point in the test’s execution, and re-run just a single test instantly after editing it — all without leaving the tool or repeatedly typing terminal commands. For a beginner specifically, this tight feedback loop — change a line, instantly see exactly what happened, step by step — is genuinely one of the fastest ways to build real intuition for how Playwright actually behaves.

codegen — a Learning Aid, With a Clear Warning

Playwright also includes a code generator that records your actions in a real browser and writes the corresponding Playwright code for you automatically:

npx playwright codegen saucedemo.com

This opens a real browser alongside a code panel — click around, type into fields, and watch Playwright generate code in real time, matching what you did. As a way to learn what a given interaction looks like in Playwright syntax, or to quickly explore what locators are available for an element, this is genuinely useful, especially early on.

But it comes with an important, honest caveat: codegen-generated locators are not automatically the best long-term choice — they’re simply a choice the tool could find automatically, and sometimes that means a longer or less stable locator than a human, using the judgment you’ll build properly in Part 7, would choose deliberately. Treat codegen as a fast way to explore and learn, not as a substitute for actually understanding why a particular locator is the right one to commit to a real test suite.


Playwright Architecture

Now that you’ve actually run a test, this diagram — first shown conceptually back in Part 0 — is worth revisiting properly, because every layer in it is now something you’ve genuinely touched, not just read about:

Test Code (login.spec.ts)

Playwright (Node.js library, launches & orchestrates)

Browser (an actual Chromium/Firefox/WebKit process)

Browser Context (an isolated environment within that browser)

Page (a single tab/window within that context)

DOM (the live tree structure from Part 1, inside that page)

Web Application (SauceDemo, actually running)

Two pieces here — Browser Context and Page — deserve a proper, concrete explanation now, since Playwright’s default behavior actually creates one of each automatically every time your test runs, even though your test code doesn’t show it explicitly.

A browser is the actual Chromium, Firefox, or WebKit process itself — a genuinely heavy thing to start up.

A browser context is an isolated environment within that already-running browser — its own cookies, its own local storage, its own session, completely separate from any other context, even though they’re running inside the very same underlying browser process. Picture a hotel: the building itself (the browser) is expensive and slow to construct, but once it exists, you can check separate guests into separate, fully private rooms (contexts) quickly and cheaply, and nothing one guest does in their room affects any other guest’s room in any way.

A page is a single tab or window within a given context — the actual thing you navigate, click on, and read content from.

Why does Playwright bother with this extra “context” layer at all, instead of just giving you a page directly inside a browser? Because of a genuine, common testing need: isolation between tests. Imagine two tests running back-to-back — one logging in as standard_user, another verifying that a logged-out user is correctly redirected to the login page.

If both tests shared the exact same cookies and session state, the second test could accidentally “inherit” the first test’s login session, passing not because the redirect logic actually works, but because the test was contaminated by leftover state from a completely different test.

Browser contexts solve this cleanly: Playwright’s test runner creates a brand new, completely clean context for every single test by default, so each test starts from true isolation — no leftover cookies, no leftover storage, no accidental contamination — while still reusing the same, already-launched, expensive browser process underneath for speed. You get both genuine test isolation and fast execution, without having to choose between them.


How It Works in a Real Test Run

A Playwright Test run starts with configuration, selects projects, creates worker processes, and gives each test an isolated browser context with a page fixture. The test sends actions and assertions through Playwright, while reporters collect results and attachments.

The reusable object hierarchy is: Browser process → BrowserContext session → Page or tab → Locator → action or assertion. Understanding which object owns state prevents confusion about cookies, tabs, permissions, and cleanup.

Current official references

Interview Questions

Q: What is Playwright, and how is it different from just writing a script that sends raw HTTP requests to a website?

Ans: Playwright is a browser automation framework that launches and controls an actual, real browser — Chromium, Firefox, or WebKit — performing the same actions a human user would, like clicking, typing, and navigating. A script sending raw HTTP requests only interacts with a server’s responses directly, without ever rendering a page or executing its JavaScript, so it can’t catch UI rendering issues, client-side JavaScript bugs, or anything that only genuinely appears once a real browser processes and displays the page.

Q: How does Playwright’s architecture differ from Selenium’s, and why does that difference matter in practice?

Ans: Selenium communicates with browsers through the WebDriver protocol, which introduces an extra translation layer between test code and the browser itself. Playwright communicates through each browser engine’s own native automation protocol directly, without that extra layer. In practice, this contributes to Playwright’s more reliable built-in auto-waiting behavior and generally faster, less flaky execution compared to Selenium suites that aren’t written with very careful, explicit waiting logic.

Q: What is the difference between a browser, a browser context, and a page in Playwright?

Ans: A browser is the actual running Chromium, Firefox, or WebKit process — relatively heavy to start. A browser context is an isolated environment within that browser, with its own separate cookies, storage, and session, completely independent of any other context in the same browser. A page is a single tab or window within a given context — the actual thing your test navigates and interacts with. One browser can host many independent contexts, and each context can host one or more pages.

Q: Why does Playwright create a new browser context for every test by default, rather than reusing one context across all tests?

Ans: To guarantee genuine test isolation. If tests shared the same context, one test’s cookies, login session, or local storage could leak into another test, causing a test to pass or fail for the wrong reason — not because its own logic is correct or broken, but because of leftover state from a completely unrelated test that happened to run before it. A fresh context per test eliminates that risk entirely, while still reusing the same already-launched browser process underneath for speed.

Q: What does retries: 0 in playwright.config.ts mean, and why might a fresh project default to it rather than a higher number?

Ans: It means a failing test is reported as failed immediately, without Playwright automatically re-running it. A fresh project defaults to zero deliberately, because relying on a high retry count as a default strategy can mask genuine flakiness rather than surface and fix it — a test that fails once and passes on retry might be revealing a real, intermittent bug or a poorly written test, and a low or zero retry count makes that visible instead of quietly hiding it behind automatic re-attempts.

Q: What is the practical difference between running Playwright tests in headless mode versus headed mode, and when would you use each?

Ans: Headless mode runs the browser without a visible window, which is faster and is how tests typically run in CI, where there’s no display anyway and nothing requires visual observation. Headed mode opens a real, visible browser window so you can watch the test execute step by step, which is genuinely useful while learning, or while actively debugging a confusing or unexpected test failure.

Q: A teammate generated a test entirely using codegen and wants to merge it as-is into the shared test suite. What would you want to check before approving it?

Ans: I’d want to check the actual locators codegen generated, since the tool finds a working locator automatically, not necessarily the most stable, long-term one a human would deliberately choose — it might rely on brittle CSS classes or deep structural paths rather than a data-test attribute or accessible role. I’d also want to check whether meaningful assertions were added, since codegen records actions like clicks and typing but doesn’t automatically know what the test is actually supposed to verify — that judgment still has to come from whoever’s writing the real test.


Exercises — Part 6

Understand: Without looking back at this part, explain in your own words the difference between a browser, a browser context, and a page — and specifically why the context layer exists, using the two-tests-sharing-state example as your explanation.

Simple Practice: Set up a fresh Playwright + TypeScript project using npm init playwright@latest. Open playwright.config.ts and identify (without changing anything yet) exactly where testDir, retries, and baseURL are set.

Real-World Scenario: Write a Playwright test, from scratch, that navigates to SauceDemo, logs in with locked_out_user / secret_sauce (an intentionally invalid scenario SauceDemo provides), and asserts that an error message becomes visible on the page, rather than a successful login. Run it once in headless mode and once with --headed, and note any difference you observe in how the test executes.

Challenge: Using npx playwright codegen saucedemo.com, record yourself logging in and adding one product to the cart. Look at the generated code afterward, and rewrite at least one of its locators using your own judgment from Part 1’s locator discussion — replacing anything you’d consider risky (a deep CSS class, a positional selector) with something you’d trust more for a real, long-term test suite.


Next: Part 7 — Locators, Deep Dive

— Playwright’s own locator API (getByRole, getByTestId, getByText, and more), the auto-waiting mechanism working invisibly underneath every one of them, and a real locator strategy you can actually defend in an interview.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed