Install Playwright and Run a First Test
On this page
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
.tstest files itself, so you do not need to install or configuretscto run tests. -
Where to put your end-to-end tests:
tests(the default; it proposese2eiftestsalready 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.tsand*.test.tsfiles. -
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 atest.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.undefinedmeans "let Playwright decide" (half your CPU cores). -
reporter: 'html': write a browsable report toplaywright-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:
-
testandexpectboth 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.beforeEachruns before every test in the file, receiving the same fixtures. -
getByPlaceholderandgetByTestIdare 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
awaitis the single most common beginner bug; the test usually finishes before the action happens. The@typescript-eslint/no-floating-promiseslint 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.