| 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
yarn add -D @playwright/test
npx playwright install --with-deps chromium
npx 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: "yarn 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: yarn
- run: yarn install --frozen-lockfile
- run: npx playwright install --with-deps chromium
- run: yarn 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