Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Playwright›Basic test structure and assertions

Introduction to Playwright

Basic test structure and assertions

This lesson establishes the fundamental building blocks of Playwright test suites, covering organization, execution, and validation. Understanding these core concepts is essential because they form the foundation upon which all reliable automation is built. Developers reach for these structures whenever they need to transform manual browser interactions into repeatable, automated verification steps.

The Anatomy of a Playwright Test

A Playwright test is organized into hierarchical blocks that define the lifecycle and scope of your browser interactions. At the top level, the 'test' function acts as the entry point, encapsulating a specific user scenario. By providing a descriptive name and an asynchronous callback function, you create a isolated execution context. Playwright manages the browser lifecycle behind the scenes, ensuring that each test gets its own pristine environment. This isolation is critical because it prevents state leakage between tests, ensuring that a failure in one area does not produce false positives elsewhere. Inside this function, the 'page' object serves as your primary tool for navigating the web, interacting with elements, and inspecting the state of the document. Understanding this structure allows you to compose complex workflows that remain readable, maintainable, and predictable across varying application states.

const { test } = require('@playwright/test');

// The 'test' function defines a single unit of execution.
test('user can navigate to login page', async ({ page }) => {
  // The 'page' object is provided by the test runner.
  await page.goto('https://example.com/login');
});

Navigating and Interacting with Elements

Interacting with the DOM in Playwright is built around the concept of locating elements and performing actions on them. Because modern applications are often dynamic, Playwright utilizes an auto-waiting mechanism. When you call an action like 'click' or 'fill', the engine automatically waits for the element to be visible, stable, and ready to receive events. This is a fundamental architectural choice that eliminates the need for arbitrary manual delays or unreliable timeouts. You should think of locators as a 'query' for an element that persists; they do not necessarily represent the current DOM state, but rather a set of instructions to find that element at the moment the action occurs. By anchoring your interactions to specific labels or roles, your tests become resilient to minor layout changes, ensuring that your test suite focuses on functionality rather than fragile CSS selectors.

test('perform a user login', async ({ page }) => {
  await page.goto('https://example.com/login');
  // Locators are lazy; they are resolved at the moment of interaction.
  await page.getByLabel('Username').fill('test_user');
  await page.getByRole('button', { name: 'Sign in' }).click();
});

Web-First Assertions

Assertions are the heartbeat of your test suite, as they bridge the gap between observed state and expected behavior. Playwright provides 'web-first' assertions, which differ from standard library checks by incorporating a built-in retry mechanism. When you use an assertion like 'expect(locator).toBeVisible()', Playwright does not simply check the current DOM state; it polls the browser until the condition is met or a timeout is reached. This is vital for verifying dynamic content that may appear after network requests or animations finish. If the condition is not met within the timeout window, the assertion throws an error, failing the test. By using these assertions, you avoid the common trap of writing flaky tests that fail because the browser was not quite ready to render the expected data, thereby increasing the overall reliability of your test results.

const { expect } = require('@playwright/test');

test('verify successful navigation', async ({ page }) => {
  await page.goto('https://example.com/dashboard');
  // Web-first assertions automatically retry until they pass.
  await expect(page.getByText('Welcome back')).toBeVisible();
});

Handling Asynchronous Flows

Because browser interactions are inherently asynchronous—involving network requests, event loops, and rendering—every test function must be treated as an asynchronous task. Using the 'async' and 'await' keywords correctly ensures that the test runner executes steps in the precise order you define. Failing to 'await' a promise in Playwright can lead to race conditions, where a test proceeds to the next assertion before the browser has finished executing a click or navigating to a new URL. By explicitly awaiting every action, you force the script to synchronize with the browser's internal engine. This rigorous sequential execution is the standard approach to managing state transitions in a web environment. Whenever you see a method returning a promise, you must ensure that your code waits for its resolution, keeping the test execution deterministic and error-free.

test('async interaction order', async ({ page }) => {
  await page.goto('https://example.com');
  // Awaiting the action ensures the page is ready before proceeding.
  await page.getByRole('link', { name: 'Settings' }).click();
  await expect(page).toHaveURL(/.*settings/);
});

Organizing Tests with Groups

As your application grows, managing individual tests becomes cumbersome, necessitating the use of 'test.describe' blocks. These blocks allow you to logically group tests that share a context, such as a specific feature module or set of preconditions. Beyond pure organization, this structure allows for the use of 'beforeEach' and 'afterEach' hooks to handle repeated setup and teardown tasks. For instance, if every test in a group requires the user to be authenticated, you can perform the login steps once within a hook instead of repeating code in every test file. This adheres to the DRY (Don't Repeat Yourself) principle, making the test suite significantly easier to update when business logic changes. Proper grouping results in clear, hierarchical test reports that help you quickly isolate which area of the application is failing when things go wrong.

test.describe('Authentication Module', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com/login');
  });

  test('shows error on bad password', async ({ page }) => {
    await page.getByLabel('Password').fill('wrong');
    await page.getByRole('button').click();
    await expect(page.getByText('Invalid login')).toBeVisible();
  });
});

Key points

  • Tests are defined as asynchronous functions that execute in isolated browser contexts.
  • The 'page' object is the primary interface for navigating to URLs and interacting with web elements.
  • Locators serve as persistent, lazy-resolved queries that make tests more resilient to DOM changes.
  • Playwright automatically waits for elements to be ready, removing the need for manual sleeps.
  • Web-first assertions provide built-in retry logic to handle dynamic content updates reliably.
  • The 'await' keyword is mandatory for all interactions to prevent race conditions during execution.
  • Organizing tests into 'test.describe' blocks improves readability and enables shared setup logic.
  • Lifecycle hooks like 'beforeEach' help eliminate code duplication by handling common test preconditions.

Common mistakes

  • Mistake: Forgetting to await an assertion. Why it's wrong: Playwright assertions are asynchronous and return a promise; failing to await them means the test proceeds without validating the state. Fix: Always prepend the 'await' keyword to expect().
  • Mistake: Performing actions before the page has finished loading or state is ready. Why it's wrong: This leads to 'flaky' tests that fail unpredictably due to race conditions. Fix: Use auto-waiting assertions or locators that wait for visibility.
  • Mistake: Overusing page.waitForTimeout(). Why it's wrong: It makes tests slow and unreliable, masking underlying issues instead of waiting for specific conditions. Fix: Use Web-First assertions like expect().toBeVisible() which poll automatically.
  • Mistake: Expecting manual assertions instead of built-in Web-First assertions. Why it's wrong: Manual checks don't benefit from the automatic retrying logic that handles dynamic UI updates. Fix: Always use the built-in expect() library provided by the test runner.
  • Mistake: Placing assertions inside a loop without proper scoping. Why it's wrong: If one iteration fails, the test stops immediately, masking errors in subsequent items. Fix: Use test.step() or verify data structures before asserting.

Interview questions

What is the basic structure of a Playwright test?

A basic Playwright test is structured using the 'test' function, which typically accepts a title string and an asynchronous arrow function. Inside this function, you use the 'page' fixture to perform browser actions. For example: test('my test', async ({ page }) => { await page.goto('url'); }); The 'test' function acts as the entry point, allowing Playwright to identify, group, and execute tests while handling browser context and page setup automatically behind the scenes.

How do you perform basic assertions in Playwright and why are they preferred?

In Playwright, we use web-first assertions like 'expect(locator).toHaveText()'. These are preferred over standard assertions because they are built to handle the asynchronous nature of web applications. Unlike traditional assertion libraries, Playwright's 'expect' automatically retries the check until the condition is met or the timeout is reached. This design significantly reduces flakiness in tests, as it eliminates the need for manual waits or 'sleep' commands when waiting for UI updates.

Explain the role of the 'page' fixture in a Playwright test.

The 'page' fixture is a built-in object provided by Playwright that represents a single tab or window within a browser context. It is the primary interface for interacting with the web application, providing methods like 'goto', 'click', 'fill', and 'screenshot'. By using the 'page' fixture in our test signature, Playwright handles the complex lifecycle management—such as initializing the browser, creating a context, and closing the page after the test completes—ensuring each test starts in a clean environment.

Compare using 'expect(locator).toBeVisible()' versus simply checking for an element's presence. Why is visibility often a better check?

Checking for presence only confirms the element exists in the DOM, whereas 'toBeVisible()' checks that the element is actually displayed on the screen and occupies space. This is a superior approach for user-facing tests because users cannot interact with hidden elements. By using 'toBeVisible()', you ensure the UI state matches the user experience, preventing false positives where an element exists in the code but is obscured by a modal or hidden via CSS.

How do you handle tests that involve multiple pages or separate tabs within a single test case?

In Playwright, if an action opens a new tab, you should use 'page.waitForEvent('popup')' to capture the new page object. For example: const [newPage] = await Promise.all([page.waitForEvent('popup'), page.click('a')]). This approach is necessary because Playwright manages pages as separate entities. Using the promise-based pattern ensures that your test waits specifically for the navigation to finish in the new tab before attempting to perform any assertions or interactions on that secondary page context.

When writing complex assertions, how does the 'soft assertion' feature improve test report visibility?

Normally, a failed assertion stops test execution immediately, which can hide subsequent errors. Playwright provides 'expect.soft()', which allows the test to continue executing after a failure. This is highly beneficial during end-to-end testing because it enables you to verify multiple non-critical UI components in a single flow, capturing all failures in a single report. It provides a more comprehensive view of the application's health without forcing developers to re-run the test to discover every individual UI bug.

All Playwright interview questions →

Check yourself

1. What is the primary benefit of using Web-First assertions in Playwright?

  • A.They execute faster by bypassing the browser's rendering engine.
  • B.They automatically retry until the condition is met or the timeout is reached.
  • C.They allow for synchronous code execution within asynchronous tests.
  • D.They force the browser to clear the cache before validation.
Show answer

B. They automatically retry until the condition is met or the timeout is reached.
Web-First assertions are designed to poll the DOM until a condition is met, reducing flakiness. The other options are incorrect because they describe features not inherent to these assertions or fundamentally misunderstand how the framework handles asynchronous browser interactions.

2. If you write 'expect(locator).toBeVisible()', what happens if the element is currently hidden but appears after 500ms?

  • A.The test fails immediately.
  • B.The test throws a timeout exception.
  • C.The assertion waits and passes once the element appears.
  • D.The assertion skips the check.
Show answer

C. The assertion waits and passes once the element appears.
Because expect() uses automatic retries, it waits for the condition to become true within the default timeout period. It does not fail immediately or skip the check, and it only throws an exception if the timeout is reached.

3. Which of the following describes the correct behavior of the 'await' keyword in Playwright assertions?

  • A.It is optional for simple assertions like toBeVisible.
  • B.It pauses the test execution entirely until the browser process restarts.
  • C.It ensures the assertion promise resolves before moving to the next line of code.
  • D.It converts a sync assertion into an async one.
Show answer

C. It ensures the assertion promise resolves before moving to the next line of code.
Playwright assertions are asynchronous functions that return promises. Awaiting them is mandatory to ensure the test runner pauses until the assertion has successfully validated the state. Without 'await', the test might finish or proceed prematurely.

4. Why should you prefer 'expect(locator).toHaveText()' over 'const text = await locator.textContent(); expect(text).toBe(...)'?

  • A.The second approach lacks the built-in retry mechanism for the text state.
  • B.The first approach runs on the server side instead of the browser.
  • C.The second approach requires a separate browser session.
  • D.The first approach is only compatible with CSS locators.
Show answer

A. The second approach lacks the built-in retry mechanism for the text state.
The second approach manually extracts the text, meaning it only checks the value at that exact millisecond. The first approach uses the built-in retry logic, which is more robust if the text updates dynamically. The other options are false as the extraction method doesn't change the execution environment.

5. When a test is structured with 'test('description', async ({ page }) => { ... })', what is the role of the 'page' fixture?

  • A.It acts as a global variable that must be defined manually.
  • B.It provides an isolated, fresh browser context for the specific test.
  • C.It serves as a static configuration file for all tests.
  • D.It is used to define test dependencies only.
Show answer

B. It provides an isolated, fresh browser context for the specific test.
The 'page' fixture provides an isolated environment for the test, ensuring tests are independent. Option 0 is wrong because the fixture is injected by the runner. Option 2 is wrong because it's an instance, not a config file. Option 3 is wrong as it is a browser interface, not a dependency manager.

Take the full Playwright quiz →

← PreviousUnderstanding Playwright Test RunnerNext →Generating and viewing test reports

Playwright

37 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app