Skip to content

End-to-end tests with Playwright

The tests of the chapter Testing run in happy-dom, a stand-in for the browser in Node. End-to-end tests drive a real browser instead. They click and type like a person and check what appears on the page. Playwright from Microsoft drives Chromium, Firefox and WebKit for this.

According to the project assignment, end-to-end tests earn one bonus point. They are slower and break more easily than tests with Vitest, so write only a few, for the flows that must never break, such as searching, signing in and sending a request. According to the Vue documentation, they catch problems in the router, the state and the requests that tests of single components miss. Keep testing components with Vitest.

The example under optional/end-to-end/en checks the demo of the case app after part 8, which runs without an API. In the configuration, webServer starts the demo in the folder of the case app and waits until it answers. Locally Playwright reuses a server that is already running, on CI it always starts its own.

playwright.config.js
import { defineConfig, devices } from '@playwright/test'
// Starts the demo of the case app, which needs no API, and runs the tests in Chromium.
// The German and the English variant use their own port, so both can run at the same time.
const PORT = 5198
export default defineConfig({
testDir: 'tests',
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
use: {
baseURL: `http://localhost:${PORT}/`,
trace: 'on-first-retry',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: {
command: `npm run demo -- --port ${PORT} --strictPort`,
cwd: '../../../case/08-architecture/en',
url: `http://localhost:${PORT}/`,
reuseExistingServer: !process.env.CI,
},
})

The tests find elements the way a screen reader knows them, by their role and their name. Playwright waits before every action until the element is ready, and expect repeats its check for up to five seconds. That is why the test does not wait for the search to finish by itself. Every test gets a fresh browser context, without favorites or a sign-in from other tests.

catalog.spec.js
import { expect, test } from '@playwright/test'
// Every test gets a fresh browser context, so favorites and the login of one test never reach another
// The card of a thing, found through its heading. Its link and button carry the name of the thing
// as hidden text, the visible label is enough inside the card.
function cardOf(page, name) {
return page.getByRole('article').filter({ has: page.getByRole('heading', { level: 2, name, exact: true }) })
}
test('searches the catalog, opens a thing and returns to its card', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('heading', { level: 1, name: 'Catalog' })).toBeVisible()
await expect(page.getByText('60 things found')).toBeVisible()
await page.getByLabel('Search').fill('saw')
await expect(page.getByText('2 things found')).toBeVisible()
const reserve = cardOf(page, 'Jigsaw').getByRole('link', { name: /Reserve/ })
await reserve.click()
await expect(page.getByRole('heading', { level: 1, name: 'Jigsaw' })).toBeVisible()
await page.goBack()
await expect(reserve).toBeFocused()
})
test('keeps a favorite after a reload', async ({ page }) => {
await page.goto('/')
const favorite = cardOf(page, 'Drill').getByRole('button', { name: /Favorite/ })
await favorite.click()
await expect(favorite).toHaveAttribute('aria-pressed', 'true')
await page.reload()
await expect(favorite).toHaveAttribute('aria-pressed', 'true')
})

The links and buttons in the cards carry the name of the thing as hidden text after a colon. Chromium puts a space before that colon, because the hidden text is positioned absolutely. A test that compares the whole name exactly therefore does not find the button. The example looks for the card by its heading first and then for the button inside it.

npx playwright install chromium installs the browser, and npm run test:e2e starts the tests. In the workflow of the course repo, GitHub Actions installs only Chromium and runs the tests after the build in both languages.

Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.