Exit course
Chapter 2 of 10 Beginner 7 min read 2 quick checks Updated

Install Playwright and Run a First Test

On this page
  1. Scaffold the project
  2. What got generated
  3. Write your first test
  4. Run it three ways
  5. Make the base URL configurable
  6. Recording instead of typing

In this chapter you scaffold a project, read what the installer generated, replace the sample with a test of your own and run it in three different ways.

Scaffold the project#

Create an empty folder and run the official initializer:

mkdir playwright-course && cd playwright-course
npm init playwright@latest

The installer asks four questions:

  • TypeScript or JavaScript: pick TypeScript (the default). Playwright compiles .ts test files itself, so you do not need to install or configure tsc to run tests.
  • Where to put your end-to-end tests: tests (the default; it proposes e2e if tests already exists).
  • Add a GitHub Actions workflow: say yes. Chapter 7 uses the file it creates.
  • Install Playwright browsers: yes. This downloads Chromium, Firefox and WebKit builds that match your Playwright version into a shared cache folder, not into node_modules.

You can re-run the command later in the same folder; it does not overwrite existing tests.

Two more install commands are worth knowing:

npx playwright install chromium     # only one browser
npx playwright install --with-deps  # browsers plus the OS libraries they need (Linux CI)

What got generated#

playwright-course/
  .github/workflows/playwright.yml
  tests/example.spec.ts
  playwright.config.ts
  package.json
  .gitignore

package.json gains a single dev dependency, @playwright/test, which contains both the test runner and the browser automation library. The .gitignore excludes output folders you never want to commit: test-results/, playwright-report/, blob-report/ and playwright/.auth/ (where saved login state will go in chapter 4).

The interesting file is playwright.config.ts. Trimmed of comments, the generated version looks like this:

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

Line by line:

  • testDir: where the runner looks for *.spec.ts and *.test.ts files.
  • fullyParallel: true: tests inside the same file may run in parallel, not just separate files. Chapter 6 explains when to turn this off.
  • forbidOnly: on CI, fail the run if someone committed a test.only. This guard has saved many teams from a pipeline that silently ran one test.
  • retries: retry failed tests twice on CI, never locally. A test that fails and then passes on retry is reported as flaky, not as passed, so you still see it.
  • workers: how many worker processes run tests at once. undefined means "let Playwright decide" (half your CPU cores).
  • reporter: 'html': write a browsable report to playwright-report/.
  • use: options applied to every test. trace: 'on-first-retry' records a full trace only when a test is being retried, which is exactly when you need one.
  • projects: the same tests, once per entry. devices['Desktop Chrome'] is a preset with a browser name, user agent and viewport size.

With the generated config, a test fails on CI and then passes on its first retry. How does the HTML report show it?

A pass on retry is reported as flaky, not as passed. The job stays green, which is exactly why the label matters: it is the only signal that something is unstable.

Write your first test#

Delete tests/example.spec.ts and create tests/todo.spec.ts:

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

test.beforeEach(async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
});

test('adds a todo item', async ({ page }) => {
  const newTodo = page.getByPlaceholder('What needs to be done?');
  await newTodo.fill('Buy milk');
  await newTodo.press('Enter');

  await expect(page.getByTestId('todo-title')).toHaveText(['Buy milk']);
});

There is a lot packed into a few lines:

  • test and expect both come from @playwright/test. There is no separate assertion library to install.
  • { page } is a fixture. You ask for it by name and the runner creates a fresh browser context and page for this test, then closes them afterwards. Chapter 4 shows how to write your own.
  • test.beforeEach runs before every test in the file, receiving the same fixtures.
  • getByPlaceholder and getByTestId are locators: lazy descriptions of how to find an element. Nothing is looked up until you act on them (chapter 2).
  • toHaveText(['Buy milk']) asserts that the list of todo titles is exactly one item with that text, retrying until it is true or the timeout expires (chapter 3).
  • Every Playwright call that touches the browser returns a promise. Forgetting an await is the single most common beginner bug; the test usually finishes before the action happens. The @typescript-eslint/no-floating-promises lint rule catches it.

A test passes in under a second, but the report shows the todo was never added. The body is newTodo.fill('Buy milk'); newTodo.press('Enter'); followed by an awaited assertion on the page title. What is wrong?

Every browser call returns a promise. Without await the test continues (and can finish) before the actions run. A sleep only hides the race, and locators never go stale because they are resolved at action time.

Run it three ways#

Headless, all projects. This is what CI does:

npx playwright test

You will see three runs of the test, one per project. When it finishes, open the report:

npx playwright show-report

Headed, one project. Useful when you want to watch the browser:

npx playwright test --project=chromium --headed

UI mode. The best way to work on tests day to day:

npx playwright test --ui

UI mode lists every test, lets you run one with a click, shows each step with a DOM snapshot you can scrub through, and has a watch mode that re-runs a test whenever you save the file. Chapter 5 goes deeper.

A few more flags you will use constantly:

npx playwright test tests/todo.spec.ts   # one file (a regex matched against the path)
npx playwright test -g "adds a todo"     # tests whose title matches
npx playwright test --last-failed        # only what failed last time
npx playwright test --list               # show what would run, run nothing

Make the base URL configurable#

Hard-coding https://demo.playwright.dev in every test makes it painful to point the suite at staging or localhost. Set baseURL once in the config:

use: {
  baseURL: process.env.BASE_URL ?? 'https://demo.playwright.dev',
  trace: 'on-first-retry',
},

and navigate with a relative path:

await page.goto('/todomvc/');

If your app runs locally, the webServer option in the same config can start it before the tests and wait for its URL to respond. The official configuration guide lists every option.

Recording instead of typing#

Playwright can also write a first draft for you: npx playwright codegen https://demo.playwright.dev/todomvc/ opens a browser and turns your clicks into test code. It is a good way to discover locators, though recorded tests usually need tidying. Our article on the Playwright recorder covers it, and chapter 5 shows where it fits in a debugging workflow.

You now have a running project. Before adding more tests, the next chapter covers the decision that most determines whether a suite is pleasant or painful to maintain: how you find elements.