Web development

How to use Playwright: from the first test to fixtures

Playwright holds a large suite together with projects, one fixture for login and a regenerated storageState. Commands and real costs.

Key points

  • The `setup` project authenticates once and `dependencies` makes the order explicit.
  • `test.extend` replaces `beforeAll` without sharing state between files.
  • `--last-failed` and `--shard` make the suite bearable, but saved state expires with the session.

Playwright takes three steps: a configuration file with projects, a fixture that prepares the page and a command that runs the suite. The version measured is @playwright/test 1.64.1.

Configuration with projects

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

export default defineConfig({
  testDir: "./e2e",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: [["html", { open: "never" }], ["github"]],
  use: { baseURL: "http://localhost:5173", trace: "on-first-retry" },
  projects: [
    { name: "setup", testMatch: /auth\.setup\.ts/ },
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"], storageState: "e2e/.auth/user.json" },
      dependencies: ["setup"],
    },
  ],
});

The setup project runs only auth.setup.ts and saves the session state; the chromium project depends on setup and reuses that state. dependencies makes the order explicit instead of trusting the file names.

Log in once

import { test as setup, expect } from "@playwright/test";

setup("autentica e salva lo stato", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill("[email protected]");
  await page.getByLabel("Password").fill("demo1234");
  await page.getByRole("button", { name: "Accedi" }).click();
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
  await page.context().storageState({ path: "e2e/.auth/user.json" });
});

storageState writes cookies and localStorage to disk. That file is a credential: keep it in .gitignore, and regenerate it on every CI run.

Fixtures: shared setup without beforeAll

import { test as base, expect } from "@playwright/test";
import { LoginPage } from "./pages/login";

type Fixtures = { loginPage: LoginPage };

export const test = base.extend({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await use(loginPage);
  },
});

export { expect };

test.extend creates a typed fixture, runs it once per test and tears it down when the test ends. It replaces beforeAll at the same cost, but with no state shared between files.

Commands you use every day

npx playwright test --project=chromium
npx playwright test --last-failed
npx playwright test --shard=1/4
npx playwright show-report

--last-failed reruns only the tests that failed in the last run; --shard=1/4 splits the suite into four parts for CI. show-report opens the HTML report with steps, trace and screenshots.

Debugging a red test

npx playwright test --ui
npx playwright test --debug
npx playwright test -g "login" --repeat-each=10

The --ui interface steps through a test one action at a time; --repeat-each=10 runs the same test ten times and surfaces the flakes a single run hides.

A named limitation

The saved state expires when the app session expires: if the cookie lasts an hour, the nightly suite starts with stale state and the first test fails with no bug behind it. Regenerate the state on every run instead of committing it, and remember that each shard repeats the login.

Where to continue

Three components worth a path: login-form, otp-input and input; to browse by category start from components/lib and the 1,556 previews. The basic installation is in Playwright: setup and first tests; unstable tests are in how to find a flaky test. The framework comparison is in Vue 3 vs React.

Frequently asked questions

Do I need to log in on every test?

No. A setup project logs in once and saves storageState; the other projects reuse it. The cost is that the state expires with the session, so CI must regenerate it.

What is the difference between beforeAll and a fixture?

beforeAll runs once per file and shares state between tests. A fixture runs once per test and is torn down at the end, so it leaves nothing behind.

Can I split the suite across machines?

Yes, with --shard=1/4 and a different value per machine. Each part repeats the login setup, so the saving grows with test duration, not with test count.

Where does the trace of a failed test go?

Into test-results, inside the folder of that test. Open it with show-report or show-trace and you get steps, network and DOM snapshots.

Do I need a running server to start the tests?

No, if you configure webServer in the config file: Playwright starts the command, waits for the port and stops it at the end. With reuseExistingServer it reuses the one you already have open locally.