A Playwright test has a shape you will write hundreds of times. Here it is, testing the Nimbus login page:
import { test, expect } from '@playwright/test';
test('shows validation errors for an empty form', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByTestId('email-error')).toBeVisible();
await expect(page.getByTestId('password-error')).toBeVisible();
});
Read it as four layers:
test('name', async ({ page }) => ...). One test, one claim about the app. The name is a sentence: "shows validation errors for an empty form" tells a reader exactly what broke when it fails.await page.goto('/')loads the page and waits for it.- Act like a user. Click Sign in without typing anything. Note the locator:
getByRole('button', { name: 'Sign in' })finds the button the way a user reads it, by its role and visible name. await expect(...)states what must now be true. These lines are the test. Without them, the code navigates, clicks, finishes, and passes whether the app works or not.
That last point is the difference between code that runs and a test that proves something. A test with no check passes against a completely broken app.
I once reviewed a test file with fourteen tests and no checks at all. Every test clicked through a form and finished. The suite was green for three months. It had never tested anything.
When you solve problems here, we check this for real: we break the app on purpose, and your test must turn red.
Two habits worth starting today:
- One behaviour per test. When it fails, the name points straight at the problem.
- Group related tests with
test.describeand put shared setup (likegoto) intest.beforeEach. Every test then starts from the same clean state.
The official first-test guide: Writing tests.
Now prove it: write this exact test yourself in the problem below, and we will check it catches the bug.