Enough setup. Let us write a test. The simplest possible test: open a page, check the title. Like testing if a restaurant is open before walking in -- basic, but essential.
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
// Navigate to the Playwright website
await page.goto('https://playwright.dev');
// Verify the page title contains "Playwright"
await expect(page).toHaveTitle(/Playwright/);
});Three lines of actual code. Let me break down every piece.
Key Point: Every test gets a fresh page object. This is a new browser tab with no cookies, no history, no state from previous tests. Complete isolation. No test can mess up another test.
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev');
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev');
// Click the "Get started" link
await page.getByRole('link', { name: 'Get started' }).click();
// Verify we landed on the installation page
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});Notice page.getByRole(). This is Playwright's recommended way to find elements. It finds elements the way a real user would -- by their visible role and text. Not by CSS classes that can change. Not by IDs that developers forget to add. By what the user actually sees.
Name your test files with .spec.ts extension. Playwright looks for this pattern by default. You can change it in config, but the convention is .spec.ts for test files and .ts for everything else (page objects, helpers, utilities).
Every Playwright action returns a Promise. If you forget the await keyword, the action fires but your test does not wait for it. The test moves to the next line immediately, causing random failures. Always await.
Q: What is the page object in Playwright?
A: The page object represents a single browser tab. Each test gets a fresh page automatically through dependency injection -- you do not create it yourself. It provides methods like goto() for navigation, getByRole() for finding elements, fill() for typing, click() for clicking, and you use expect(page) for page-level assertions like toHaveTitle() or toHaveURL(). Since each test gets its own page, tests are completely isolated from each other.
Key Point: A Playwright test imports test and expect, gets a fresh page object, navigates with goto(), and asserts with expect(). Every action needs await.