Now that Playwright is installed, it is time to write real tests. In this lesson you will understand every line of a Playwright test, automate a login page, run it in different ways, read a failure message, and view the HTML report.
๐ What You’ll Learn: test anatomy (test, expect, page fixture, async/await), writing a login test, running it from the CLI and VS Code, reading failure messages, negative tests with describe and beforeEach, the HTML report and baseURL.
๐งฑ Anatomy of a Playwright Test
Open tests/example.spec.ts from your project. Every Playwright test has the same building blocks:
import { test, expect } from '@playwright/test'; // 1. import test and expect
test('has title', async ({ page }) => { // 2. test name + async function
await page.goto('https://playwright.dev/'); // 3. actions use await
await expect(page).toHaveTitle(/Playwright/); // 4. assertion
});
- Import
test(to declare tests) andexpect(to make assertions) from@playwright/test. - test(name, function): the name appears in reports, so make it describe the behaviour. The function is
asyncbecause browser actions take time. - { page } is a fixture: Playwright creates a fresh browser tab for each test and passes it in. You never launch the browser yourself.
- await in front of every action and assertion, so each step finishes before the next starts.
- expect(…) checks the result. Web-first assertions like
toHaveTitleretry automatically until they pass or time out.
โ๏ธ Write Your First Real Test: Login
We will automate the login page of Sauce Demo (https://www.saucedemo.com), a free practice shop. Create a new file tests/login.spec.ts:
import { test, expect } from '@playwright/test';
test('standard user can log in', async ({ page }) => {
// Arrange: open the login page
await page.goto('https://www.saucedemo.com/');
// Act: fill the form and submit
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
// Assert: we are on the products page
await expect(page).toHaveURL(/inventory\.html/);
await expect(page.locator('.title')).toHaveText('Products');
});
What each line does:
page.goto(url)opens the page and waits for it to load.page.getByPlaceholder('Username')finds the input by its placeholder text. This is a locator; you will master locators in Playwright Locators.fill()clears the input and types the text.getByRole('button', { name: 'Login' })finds the button the way a user or screen reader sees it.toHaveURLandtoHaveTextverify the outcome.
๐ก Tip: Structure tests as Arrange, Act, Assert: set up, perform the user action, then verify. It keeps tests easy to read.
โถ๏ธ Run the Test
Run only this file, in Chromium, with the browser visible:
npx playwright test login.spec.ts --project=chromium --headed
Or run it from VS Code: open the Testing view, tick Show browser, and click โถ next to the test name. When it passes you see a green tick; in the terminal you see:
Running 1 test using 1 worker
โ 1 [chromium] โบ tests/login.spec.ts:3:5 โบ standard user can log in (1.9s)
1 passed (2.6s)
โ Make It Fail on Purpose (and Read the Error)
Learning to read failures is as important as writing tests. Change the expected text to 'Product' (without the s) and run again. Playwright prints exactly what went wrong:
Error: Timed out 5000ms waiting for expect(locator).toHaveText(expected)
Locator: locator('.title')
Expected string: "Product"
Received string: "Products"
The message shows the locator, what was expected and what was received. Change it back to 'Products' so the test passes again.
๐งช Add a Negative Test and Group Tests
Good test suites cover failures too. Use test.describe to group related tests, and test.beforeEach to share setup:
import { test, expect } from '@playwright/test';
test.describe('Login page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
});
test('standard user can log in', async ({ page }) => {
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL(/inventory\.html/);
});
test('locked out user sees an error', async ({ page }) => {
await page.getByPlaceholder('Username').fill('locked_out_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.locator('[data-test="error"]'))
.toContainText('Sorry, this user has been locked out.');
});
test('empty form shows username required', async ({ page }) => {
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.locator('[data-test="error"]')).toContainText('Username is required');
});
});
Hooks such as beforeEach get a full lesson later in the course.
๐ View the HTML Report
npx playwright test
npx playwright show-report
The report lists each test per browser, with its steps, duration and, for failures, the error, screenshot and trace (depending on your config). Click a test to see every action that ran.
๐ Use baseURL to Shorten Your Tests
Instead of repeating the full URL, set baseURL in playwright.config.ts:
// playwright.config.ts (inside defineConfig)
use: {
baseURL: 'https://www.saucedemo.com',
},
Now await page.goto('/') opens the home page. Learn every config option in playwright.config.ts explained.
โ Beginner Mistakes to Avoid
- Forgetting await: the test may finish before the action happens. Always write
awaitbeforepage.*actions andexpectfor locators. - Using sleeps: do not add fixed waits; Playwright auto-waits. See Playwright waits.
- Vague test names:
'test1'tells you nothing in a report. Describe the behaviour. - Tests that depend on each other: each test should work alone, in any order.
๐ Assignment
Use https://www.saucedemo.com for all tasks and put them in tests/login.spec.ts:
- Write a test that logs in as
problem_userand verifies the products page title. - Write a test that enters a wrong password and checks the error contains
Username and password do not match. - Write a test that logs in, adds Sauce Labs Backpack to the cart, and verifies the cart badge shows
1(page.locator('.shopping_cart_badge')). - Run all tests in Chromium with
--headed, then in all browsers, and compare the HTML report. - Bonus: make one test fail on purpose and write down the three pieces of information the error message gave you.
โ FAQ
๐ค How do I write my first Playwright test?
Create a .spec.ts file in the tests folder, import test and expect from @playwright/test, write test('name', async ({ page }) => { ... }) with actions and assertions, and run npx playwright test.
๐ค Why does every Playwright test use async and await?
Browser actions take time and return Promises. await makes each step finish before the next one runs. Learn more in TypeScript Functions and TypeScript for Playwright.
๐ค How do I run a single Playwright test?
Run npx playwright test login.spec.ts for one file, add -g "test name" to filter by title, or click โถ in the VS Code Testing view. All options are in Playwright CLI commands.
๐ค Why does my test run three times?
The default config has three projects (Chromium, Firefox, WebKit), so each test runs once per browser. Use --project=chromium to run one browser.
๐ค How do I see the browser while the test runs?
Add --headed to the command, use --ui for UI mode, or tick Show browser in the VS Code extension.
๐ฌ Watch the full Playwright tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on sqatools.in