| 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 { expect, test } 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 { expect, test } 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 { devices, expect, test } 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 AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
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