---
title: "End-to-end tests with Playwright for a Vue app · vuejs.school"
description: "Install Playwright 1.57 next to a Vite and Vue app, point it at vite preview, write three tests with role locators, read a trace, run it in CI."
canonical_url: "https://vuejs.school/community/blog/end-to-end-tests-with-playwright"
meta:
  "og:description": "Install Playwright 1.57 next to a Vite and Vue app, point it at vite preview, write three tests with role locators, read a trace, run it in CI."
  "og:image": "https://vuejs.school/_og/d/a_~RW5kLXRvLWVuZCB0ZXN0cyB3aXRoIFBsYXl3cmlnaHQgZm9yIGEgVnVlIGFwcCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_End-to-end%2Btests%2Bwith%2BPlaywright%2Bfor%2Ba%2BVue%2Bapp%2Cdescription_~SW5zdGFsbCBQbGF5d3JpZ2h0IDEuNTcgbmV4dCB0byBhIFZpdGUgYW5kIFZ1ZSBhcHAsIHBvaW50IGl0IGF0IHZpdGUgcHJldmlldywgd3JpdGUgdGhyZWUgdGVzdHMgd2l0aCByb2xlIGxvY2F0b3JzLCByZWFkIGEgdHJhY2UsIHJ1biBpdCBpbiBDSS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbmQtdG8tZW5kLXRlc3RzLXdpdGgtcGxheXdyaWdodCI%2Cs_u8j2DDIz95mdgmEB.png"
  "og:image:alt": "End-to-end tests with Playwright for a Vue app · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "End-to-end tests with Playwright for a Vue app · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~RW5kLXRvLWVuZCB0ZXN0cyB3aXRoIFBsYXl3cmlnaHQgZm9yIGEgVnVlIGFwcCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_End-to-end%2Btests%2Bwith%2BPlaywright%2Bfor%2Ba%2BVue%2Bapp%2Cdescription_~SW5zdGFsbCBQbGF5d3JpZ2h0IDEuNTcgbmV4dCB0byBhIFZpdGUgYW5kIFZ1ZSBhcHAsIHBvaW50IGl0IGF0IHZpdGUgcHJldmlldywgd3JpdGUgdGhyZWUgdGVzdHMgd2l0aCByb2xlIGxvY2F0b3JzLCByZWFkIGEgdHJhY2UsIHJ1biBpdCBpbiBDSS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbmQtdG8tZW5kLXRlc3RzLXdpdGgtcGxheXdyaWdodCI%2Cs_u8j2DDIz95mdgmEB.png"
  "twitter:image:alt": "End-to-end tests with Playwright for a Vue app · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Install Playwright

Run the initialiser in the project folder:

```
npm init playwright@latest
```

Answer its questions like this:

1. TypeScript or JavaScript: TypeScript. A project that already has a `tsconfig.json`, as a TypeScript `create-vue` app does, skips this question.
2. Where to put the tests: type `e2e`, so the browser tests stay apart from your Vitest files. The Vitest config that `create-vue` writes already excludes `e2e/**`, so Vitest will not try to run them.
3. Add a GitHub Actions workflow: yes.
4. Install the Playwright browsers: yes.

On Linux it also offers to install the system libraries the browsers need, which requires `sudo`.

The command adds `@playwright/test` to your `devDependencies` and writes `playwright.config.ts`, an example test in `e2e/example.spec.ts` and a workflow in `.github/workflows/playwright.yml`. The [getting started page](https://playwright.dev/docs/intro) lists the requirements: Node 20, 22 or 24, and a recent Windows, macOS, Debian or Ubuntu release.

## Point the config at vite preview

Test the production build, not the dev server. The dev server serves unbundled modules and skips the minifier, so a problem that only the build introduces never shows up there. `vite preview` serves the built `dist` folder on port 4173: the same files your host will serve. Replace the generated config with this one:

*playwright.config.ts*

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

export default defineConfig({
  testDir: 'e2e',
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:4173',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'npm run build && npm run preview',
    url: 'http://localhost:4173',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'mobile', use: { ...devices['Pixel 7'] } },
  ],
})
```

The `webServer` block makes the suite self-contained. Before the first test, Playwright runs `command`, polls `url` until it answers, and only then starts the tests. It stops the server at the end. The other settings:

- `timeout` is in milliseconds. The default is 60 seconds, and a build plus a server start can take longer than that on a laptop, so give it two minutes.
- `reuseExistingServer: !process.env.CI` reuses a preview server you already have running locally, while CI always starts a fresh one.
- `baseURL` lets every test write `page.goto('/')` instead of the full address.
- `trace: 'on-first-retry'` records a trace only when a test fails and runs again, which costs nothing on a green run.
- `retries` is 2 in CI and 0 locally. In CI, a test that fails and then passes on a retry is marked flaky in the report instead of failing the job. Locally, you see the failure straight away.
- The two projects run every test twice, once in desktop Chrome and once with the screen size, touch and user agent of a Pixel 7.

The [web server guide](https://playwright.dev/docs/test-webserver) describes each option, including a `wait` field new in 1.57 that matches a regular expression against the server's output. `vite preview` does not need it, because it only opens the port when it is ready.

For a Nuxt app, set the command to `npm run build && node .output/server/index.mjs` and use port 3000 in `url` and `baseURL`.

## Write a first test for the home page

The example test that the initialiser wrote visits playwright.dev. Delete it and write one against your own app:

*e2e/home.spec.ts*

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

test('the home page shows the headline', async ({ page }) => {
  await page.goto('/')
  await expect(page.getByRole('heading', { level: 1, name: 'Plan your week' })).toBeVisible()
  await expect(page.getByRole('link', { name: 'Sign in' })).toBeVisible()
})
```

Every test receives a fresh `page`: a browser tab with no cookies and no storage from the previous test. `getByRole('heading', { level: 1, name: 'Plan your week' })` finds the `<h1>` by what it is and what it says, the way a screen reader does. `expect(locator).toBeVisible()` is a retrying assertion: it checks again and again until the element is visible or five seconds pass.

Run the suite from the project folder:

- `npx playwright test` runs every test in both projects.
- Add `--ui` to watch the tests in a window with a timeline, `--headed` to see the browser, or `--project=chromium` to skip the mobile run while you iterate.
- `npx playwright show-report` opens the HTML report from the last run.

## Test sign-in without a real backend

The app has a `/login` page with an email field, a password field and a Sign in button. Submitting posts to `/api/login`, stores the token and sends the user to `/dashboard`, which greets them by name. A navigation guard sends a visitor without a token back to `/login`. The test should not depend on a real backend being up, so answer the request inside the browser:

*e2e/login.spec.ts*

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

test('a member can sign in and reach the dashboard', async ({ page }) => {
  await page.route('**/api/login', (route) =>
    route.fulfill({ json: { name: 'Asha', token: 'test-token' } }),
  )

  await page.goto('/login')
  await page.getByLabel('Email').fill('asha@example.com')
  await page.getByLabel('Password').fill('correct horse battery')
  await page.getByRole('button', { name: 'Sign in' }).click()

  await expect(page).toHaveURL(/\/dashboard$/)
  await expect(page.getByRole('heading', { name: 'Welcome back, Asha' })).toBeVisible()
})

test('a visitor without a session is sent to the login page', async ({ page }) => {
  await page.goto('/dashboard')
  await expect(page).toHaveURL(/\/login$/)
  await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible()
})
```

`page.route` intercepts every request whose URL matches the pattern, and `route.fulfill({ json })` answers it with a 200 status and a JSON body. The Vue code runs unchanged; only the network answer is staged.

`getByLabel('Email')` finds the input through its `<label>`. If the test cannot find the field, a screen reader cannot either, and the fix belongs in the component.

`toHaveURL` takes a regular expression and retries like the other assertions. That matters here, because the router navigates only after the response arrives. The second test checks the navigation guard from the other side in three lines.

Keep one real login, against a staging server with a test account, as a separate smoke test. Everything else can use `page.route`.

## Find elements by role and let Playwright wait

Playwright's [locator guide](https://playwright.dev/docs/locators) recommends locators based on what the user sees, with `getByRole` first, because it is the closest to how users and assistive technology perceive the page. In practice:

- `getByRole` for buttons, links, headings and most other elements.
- `getByLabel` for form fields.
- `getByPlaceholder`, `getByText` and `getByAltText` where they fit better.
- `getByTestId` only for an element with no visible name.

The guide does not recommend CSS and XPath selectors. They describe how the page is built rather than what the user sees, so a change to the markup can break them.

You do not write waits either. Before `click()`, Playwright waits until the element is visible, stable (not animating), enabled and not covered by another element. Before `fill()`, it waits until the field is visible, enabled and editable. The test that tries to wait by hand is the one that flakes:

*e2e/login.spec.ts**Breaks***

```
await page.click('.btn-primary')
await page.waitForTimeout(1000)
const text = await page.textContent('h1')
expect(text).toBe('Welcome back, Asha')
```

Three things are wrong:

- `.btn-primary` is a styling class, and the next restyle can rename it.
- `waitForTimeout(1000)` is a guess: too short on a slow CI runner, too long everywhere else.
- `textContent` reads the heading once, and a plain `expect` on a string does not retry, so the test fails whenever the heading arrives after that read.

The retrying version is shorter:

*e2e/login.spec.ts**Works***

```
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.getByRole('heading', { name: 'Welcome back, Asha' })).toBeVisible()
```

## When a test fails, open the trace

With `trace: 'on-first-retry'`, a test that fails in CI runs again with recording on, and the HTML report gets a trace icon next to it. The [trace viewer](https://playwright.dev/docs/trace-viewer-intro) shows every action on a timeline. For each step it has a DOM snapshot from before and after the action, which you can inspect with the browser's own DevTools, plus the network requests, the console output and the source line. Instead of guessing why a click did nothing, you look at the page as it was at that moment.

To open a trace:

- Locally, where tests do not retry, force a recording: run `npx playwright test --trace on`, then `npx playwright show-report`.
- From CI, download the `playwright-report` artifact, unzip it, and run `npx playwright show-report` followed by the folder's name.

The report in 1.57 also gained a Speedboard tab that lists tests by how long they took. Look there when the suite starts feeling slow.

## Run the tests in GitHub Actions

The initialiser wrote `.github/workflows/playwright.yml`. Replace its contents with this shorter version:

*.github/workflows/playwright.yml*

```
name: e2e
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
```

`npx playwright install --with-deps` downloads the browsers and the system libraries they need on Ubuntu. The upload step runs even when tests fail, so the report and its traces are there when you need them. The [CI guide](https://playwright.dev/docs/ci) recommends `workers: 1` in CI for reproducible runs, which the config above already sets.

Instead of installing browsers on every run, you can use the official image. Set `container: mcr.microsoft.com/playwright:v1.57.0-noble` on the job and drop the `npx playwright install --with-deps` step. The image ships the browsers, their system libraries and, since 1.57, Node 24. Keep its tag equal to the `@playwright/test` version your project installs: when the two differ, Playwright cannot find its browsers and the run stops with "Executable doesn't exist at …".

## Check accessibility with axe

Playwright can run axe, the accessibility rule engine, inside the page it just rendered. Install the integration package:

```
npm install -D @axe-core/playwright
```

Then add a test that scans a page and expects no violations:

*e2e/a11y.spec.ts*

```
import AxeBuilder from '@axe-core/playwright'
import { expect, test } from '@playwright/test'

test('the home page has no detectable accessibility violations', async ({ page }) => {
  await page.goto('/')
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze()
  expect(results.violations).toEqual([])
})
```

`withTags` limits the scan to the rules behind the WCAG 2.0 and 2.1 level A and AA success criteria. Without it, axe also runs best-practice rules that no WCAG criterion requires.

When the assertion fails, each violation in the diff carries an `id` such as `color-contrast` or `label`, a help URL and the HTML of the offending nodes. For a third-party widget you cannot fix, `.exclude('#chat-widget')` leaves it out of the scan, along with everything inside it, for every rule.

Run the same scan on the login page and, after the sign-in steps, on the dashboard. The [accessibility testing guide](https://playwright.dev/docs/accessibility-testing) is clear that automated checks find only some of the problems, and that many show up only when a person tests the page. This test is a floor, not a certificate.

**Check it stuck**3 questions from the End-to-end tests with Playwright quiz.

[**Practice now**](https://vuejs.school/learn/testing/end-to-end-tests-with-playwright?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 6 min read**Vitest 5: what changed for Vue tests**](https://vuejs.school/community/blog/vitest-5-what-changed-for-vue-tests) [Release notes · 6 min read**Vitest 5 release candidate: what's coming for Vue tests**](https://vuejs.school/community/blog/vitest-5-rc-whats-coming)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/end-to-end-tests-with-playwright#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-12-11","description":"Install Playwright 1.57 next to a Vite and Vue app, point it at vite preview, write three tests with role locators, read a trace, run it in CI.","name":"End-to-end tests with Playwright for a Vue app","url":"https://vuejs.school/community/blog/end-to-end-tests-with-playwright","breadcrumb":{"@id":"https://vuejs.school/community/blog/end-to-end-tests-with-playwright#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/end-to-end-tests-with-playwright"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/end-to-end-tests-with-playwright#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-12-11","description":"Install Playwright 1.57 next to a Vite and Vue app, point it at vite preview, write three tests with role locators, read a trace, run it in CI.","headline":"End-to-end tests with Playwright for a Vue app","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~RW5kLXRvLWVuZCB0ZXN0cyB3aXRoIFBsYXl3cmlnaHQgZm9yIGEgVnVlIGFwcCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_End-to-end%2Btests%2Bwith%2BPlaywright%2Bfor%2Ba%2BVue%2Bapp%2Cdescription_~SW5zdGFsbCBQbGF5d3JpZ2h0IDEuNTcgbmV4dCB0byBhIFZpdGUgYW5kIFZ1ZSBhcHAsIHBvaW50IGl0IGF0IHZpdGUgcHJldmlldywgd3JpdGUgdGhyZWUgdGVzdHMgd2l0aCByb2xlIGxvY2F0b3JzLCByZWFkIGEgdHJhY2UsIHJ1biBpdCBpbiBDSS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbmQtdG8tZW5kLXRlc3RzLXdpdGgtcGxheXdyaWdodCI%2Cs_u8j2DDIz95mdgmEB.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/end-to-end-tests-with-playwright#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/end-to-end-tests-with-playwright#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/end-to-end-tests-with-playwright#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"End-to-end tests with Playwright for a Vue app","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~RW5kLXRvLWVuZCB0ZXN0cyB3aXRoIFBsYXl3cmlnaHQgZm9yIGEgVnVlIGFwcCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_End-to-end%2Btests%2Bwith%2BPlaywright%2Bfor%2Ba%2BVue%2Bapp%2Cdescription_~SW5zdGFsbCBQbGF5d3JpZ2h0IDEuNTcgbmV4dCB0byBhIFZpdGUgYW5kIFZ1ZSBhcHAsIHBvaW50IGl0IGF0IHZpdGUgcHJldmlldywgd3JpdGUgdGhyZWUgdGVzdHMgd2l0aCByb2xlIGxvY2F0b3JzLCByZWFkIGEgdHJhY2UsIHJ1biBpdCBpbiBDSS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbmQtdG8tZW5kLXRlc3RzLXdpdGgtcGxheXdyaWdodCI%2Cs_u8j2DDIz95mdgmEB.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~RW5kLXRvLWVuZCB0ZXN0cyB3aXRoIFBsYXl3cmlnaHQgZm9yIGEgVnVlIGFwcCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_End-to-end%2Btests%2Bwith%2BPlaywright%2Bfor%2Ba%2BVue%2Bapp%2Cdescription_~SW5zdGFsbCBQbGF5d3JpZ2h0IDEuNTcgbmV4dCB0byBhIFZpdGUgYW5kIFZ1ZSBhcHAsIHBvaW50IGl0IGF0IHZpdGUgcHJldmlldywgd3JpdGUgdGhyZWUgdGVzdHMgd2l0aCByb2xlIGxvY2F0b3JzLCByZWFkIGEgdHJhY2UsIHJ1biBpdCBpbiBDSS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbmQtdG8tZW5kLXRlc3RzLXdpdGgtcGxheXdyaWdodCI%2Cs_u8j2DDIz95mdgmEB.png"}]}
```