| name | playwright-testing |
| description | Generer og kjør Playwright E2E-tester for webapplikasjoner med page objects, auth fixtures og tilgjengelighetstester |
| license | MIT |
| compatibility | Node.js with Playwright |
| metadata | {"domain":"testing","tags":"playwright e2e testing accessibility responsive"} |
Playwright E2E Testing Skill
Generate Playwright tests for Nav web applications. Covers page object pattern, authentication fixtures, accessibility testing, and CI configuration.
Getting Started
- Install Playwright and configure
playwright.config.ts
- Create page objects for your app's pages
- Set up auth fixtures for Azure AD / MockOAuth2Server
- Write tests: navigation, forms, responsive, accessibility
- Add CI workflow in GitHub Actions
1. Project Setup
pnpm add -D @playwright/test
pnpm exec playwright install --with-deps chromium
pnpm exec playwright init
playwright.config.ts
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 ? 1 : undefined,
reporter: [['html', { open: 'never' }]],
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['Pixel 7'] } },
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
2. Page Object Pattern
import { type Locator, type Page, expect } from '@playwright/test'
export class OversiktPage {
readonly heading: Locator
readonly searchField: Locator
readonly table: Locator
constructor(private readonly page: Page) {
this.heading = page.getByRole('heading', { name: /oversikt/i })
this.searchField = page.getByRole('searchbox', { name: /søk/i })
this.table = page.getByRole('table')
}
async goto() {
await this.page.goto('/oversikt')
await expect(this.heading).toBeVisible()
}
async search(query: string) {
await this.searchField.fill(query)
await this.searchField.press('Enter')
}
async expectRowCount(count: number) {
const rows = this.table.getByRole('row')
await expect(rows).toHaveCount(count + 1)
}
}
3. Auth Fixture (Azure AD / MockOAuth2Server)
import { test as base } from '@playwright/test'
type AuthFixtures = {
authenticatedPage: ReturnType<(typeof base)['page']>
}
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page, context }, use) => {
await context.addCookies([
{
name: 'selvbetjening-idtoken',
value: process.env.TEST_TOKEN ?? 'test-token',
domain: 'localhost',
path: '/',
},
])
await use(page)
},
})
export { expect } from '@playwright/test'
4. Test Examples
Page Navigation
import { test, expect } from '@playwright/test'
test.describe('Navigation', () => {
test('should navigate to oversikt page', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: /oversikt/i }).click()
await expect(page).toHaveURL(/\/oversikt/)
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
})
test('should show 404 for unknown routes', async ({ page }) => {
const response = await page.goto('/ukjent-side')
expect(response?.status()).toBe(404)
})
})
Form
import { test, expect } from '@playwright/test'
test.describe('Søknadsskjema', () => {
test('should submit form successfully', async ({ page }) => {
await page.goto('/soknad')
await page.getByLabel('Navn').fill('Ola Nordmann')
await page.getByLabel('E-post').fill('ola@nav.no')
await page.getByRole('combobox', { name: /tema/i }).selectOption('dagpenger')
await page.getByRole('button', { name: /send inn/i }).click()
await expect(page.getByRole('alert')).toContainText('Sendt')
})
test('should show validation errors', async ({ page }) => {
await page.goto('/soknad')
await page.getByRole('button', { name: /send inn/i }).click()
await expect(page.getByText('Navn er påkrevd')).toBeVisible()
await expect(page.getByText('E-post er påkrevd')).toBeVisible()
})
})
Responsive Design
import { test, expect, devices } from '@playwright/test'
test.describe('Responsive', () => {
test('should show mobile menu on small screens', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 })
await page.goto('/')
await expect(page.getByRole('button', { name: /meny/i })).toBeVisible()
await expect(page.getByRole('navigation')).not.toBeVisible()
await page.getByRole('button', { name: /meny/i }).click()
await expect(page.getByRole('navigation')).toBeVisible()
})
test('should show full navigation on desktop', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 })
await page.goto('/')
await expect(page.getByRole('navigation')).toBeVisible()
})
})
5. Accessibility Testing med axe
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test.describe('Accessibility', () => {
const pages = ['/', '/oversikt', '/usage']
for (const path of pages) {
test(`${path} should have no a11y violations`, async ({ page }) => {
await page.goto(path)
const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa']).analyze()
expect(results.violations).toEqual([])
})
}
})
6. CI Configuration (GitHub Actions)
e2e:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm exec playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
Locator Strategies
Prioritized order for finding elements:
page.getByRole('button', { name: /send inn/i })
page.getByRole('heading', { level: 1 })
page.getByRole('link', { name: /oversikt/i })
page.getByLabel('Fødselsnummer')
page.getByPlaceholder('Søk...')
page.getByText('Ingen resultater')
page.getByTestId('metrics-chart')
page.locator('.my-class')
page.locator('#my-id')
Tips
- Implicit waiting: Playwright waits automatically for elements — avoid
page.waitForTimeout()
- Isolate tests: Each test should be able to run independently
- Use
test.describe to group related tests
- Parallel tests: Keep tests independent so
fullyParallel: true works
- Screenshots: Automatic on failure — check the
test-results/ directory