Jeg stolede på min SPA uden tests, det gik som forventet
Du sidder med din første lille single page app, klikker lidt rundt, alt virker, og du tænker: “Fint, den er der”. Du deployer til Netlify eller Vercel, sender linket til en ven, og tre minutter senere får du en besked: “Øh, jeg kan ikke logge ind?”.
Det var her, jeg første gang tænkte: Jeg bliver nødt til at have noget, der klikker rundt for mig, hver gang jeg ændrer noget. Det blev Playwright.
Hvad jeg gerne vil have Playwright til at gøre for mig
Jeg bruger Playwright som en meget disciplineret test-bruger. Ikke til alt. Bare til nogle få, men vigtige ting:
1) Kan min app faktisk loade den side, jeg tror.
2) Kan man bruge de vigtigste flows uden at kende til min kode.
3) Overlever det en deploy, eller døde jeg på vejen.
I stedet for at tale længe om teori, starter jeg med et konkret mål. Lad os sige, at du har en simpel SPA med:
– En forside med en liste af todo-items
– Et login (fx fake, bare et felt og en knap)
– Mulighed for at tilføje og slette et todo-item
Det, jeg vil have Playwright til at teste, er:
– At forsiden loader, og at en bestemt tekst kan ses
– At login-flowet kan gennemføres
– At jeg kan oprette og slette et todo-item
– At det også virker på den rigtige, deployede URL
Hele pointen er, at du kan tage de samme mønstre med over i din egen app. Om det er en todo, et lille dashboard eller en lille læringsplatform, er ligegyldigt. Strukturen er den samme.
Første skridt med Playwright uden at drukne
Jeg tager udgangspunkt i en almindelig Node-opsætning. Du har en SPA, som du kan starte lokalt, typisk med npm run dev eller lignende.
Start med at installere Playwright i et eksisterende projekt:
npm init playwright@latest
Hvis du allerede har en package.json, spørger den, om den skal bruge den. Sig ja.
Du får et par spørgsmål. Mit typiske valg i små projekter:
? Where to put your end-to-end tests? > tests
? Add a GitHub Actions workflow? > No (kan komme senere)
? Install Playwright browsers? > Yes
Efter det har kørt, har du:
– En playwright.config.(ts|js)
– En tests-mappe med en eller flere eksempler
– Nye scripts i package.json, typisk:
"scripts": {
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:ui": "playwright test --ui"
}
Hvis du vil teste en SPA, der kører lokalt, skal du fortælle Playwright, hvor den skal gå hen. I playwright.config er der typisk en use-sektion:
use: {
baseURL: 'http://localhost:5173',
}
Juster den til din dev-server. Vite bruger ofte port 5173, Create React App bruger 3000 osv.
Nu kan du allerede køre:
npm run test:e2e
Du vil se Playwright starte en browser i baggrunden, køre en demo-test og dumpe en rapport. Det er her, jeg som regel tænker: Fint, så fjerner jeg alt, hvad der følger med, og starter forfra med noget, jeg selv forstår.
Sådan gør du dine tests mindre skrøbelige med data-testid
De fleste, der prøver e2e første gang, gør det samme som jeg gjorde: bruger CSS-selectors i stil med .btn-primary eller .todo-item:nth-child(1). Det virker, lige indtil du ændrer lidt styling, eller listen sorteres anderledes. Så falder alt fra hinanden.
Min løsning i små projekter er at indføre en lille regel for mig selv:
– Alt, jeg vil teste med Playwright, får et data-testid-attribut.
Eksempel i din SPA:
<h1 data-testid="page-title">Min todo app</h1>
<form data-testid="todo-form">
<input data-testid="todo-input" />
<button data-testid="todo-submit">Tilføj</button>
</form>
<ul data-testid="todo-list">
<li data-testid="todo-item">
<span>Køb mælk</span>
<button data-testid="todo-delete">Slet</button>
</li>
</ul>
Det ændrer ikke noget for dine brugere, men det gør dit liv med tests meget nemmere. I testen kan du skrive:
await page.getByTestId('todo-input').fill('Køb kaffe');
await page.getByTestId('todo-submit').click();
Og du er fuldstændig ligeglad med, om knappen er blå eller grøn, om der er en ekstra <div> rundt om, eller om du har skiftet CSS-framework.
Hvis du vil læse lidt mere om den tilgang, har jeg også skrevet om semantik og struktur i HTML i en anden artikel på Coding Class, men lad os holde os til Playwright her.
De tre tests der redder mig oftest
Jeg skriver sjældent 50 e2e-tests. I små projekter ender jeg ofte med 3-6 stykker, som alle tjekker hele flows. Her er de tre, jeg starter med, næsten hver gang.
1. Navigationen og at siden faktisk loader
Den første test sørger bare for, at appen kan åbnes, og at en bestemt tekst eller komponent faktisk er der.
// tests/smoke.spec.ts
import { test, expect } from '@playwright/test';
test('forsiden loader med titel', async ({ page }) => {
await page.goto('/');
const title = page.getByTestId('page-title');
await expect(title).toHaveText('Min todo app');
});
Nogle ting, der er værd at lægge mærke til:
– Jeg bruger page.goto('/') i stedet for en fuld URL. Det bruger baseURL fra config.
– Jeg bruger getByTestId, så testen er uafhængig af HTML-struktur.
– Jeg bruger expect(...).toHaveText(), som automatisk venter lidt, hvis elementet er forsinket.
Den her test fanger overraskende meget. Forkert route, bundlet der ikke loader, JavaScript-fejl på forsiden. Alt det, din mavefornemmelse ellers plejer at ignorere.
2. Formular med input og validering
De fleste SPA’er lever af formularer. Login, søgning, tilføj noget, rediger noget. Så jeg laver næsten altid én test, der gennemspiller en formular fra tom til gyldig.
Antag, at du har et login med en simpel validering:
<form data-testid="login-form">
<input data-testid="login-email" type="email" />
<input data-testid="login-password" type="password" />
<button data-testid="login-submit">Log ind</button>
<p data-testid="login-error"></p>
</form>
Playwright-testen kunne se sådan her ud:
test('login viser fejl og accepterer korrekt login', async ({ page }) => {
await page.goto('/login');
await page.getByTestId('login-submit').click();
await expect(page.getByTestId('login-error'))
.toHaveText('Udfyld begge felter');
await page.getByTestId('login-email').fill('test@example.com');
await page.getByTestId('login-password').fill('hemmelig');
await page.getByTestId('login-submit').click();
await expect(page.getByTestId('page-title'))
.toHaveText('Min todo app');
});
Her tester du faktisk flere ting på én gang:
– At valideringstekst dukker op, når felter er tomme
– At valideringen forsvinder, når du udfylder korrekt
– At navigationen efter login virker
Hvis du en dag laver knappen om til et ikon eller ændrer teksten på fejlbeskeden, er det stadig den samme data-testid, så testen kan holde til det.
3. CRUD-flow i en lille todo-liste
Den tredje test kører hele vejen rundt: opret noget, se det, slet det igen. Det føles altid lidt tilfredsstillende, når den lykkes.
test('todo kan oprettes og slettes', async ({ page }) => {
await page.goto('/');
const input = page.getByTestId('todo-input');
const submit = page.getByTestId('todo-submit');
const list = page.getByTestId('todo-list');
await input.fill('Køb kaffe');
await submit.click();
const newItem = list.getByText('Køb kaffe');
await expect(newItem).toBeVisible();
const deleteButton = newItem.getByTestId('todo-delete');
await deleteButton.click();
await expect(list.getByText('Køb kaffe')).toHaveCount(0);
});
Her bruger jeg en blanding af tekst og data-testid. Hvis du vil være helt stabil over for oversættelser eller tekstændringer, kan du give selve elementet et testid, fx data-testid="todo-item-text", og så finde det den vej.
Pointen er, at du tester et helt brugerflow i stedet for en enkelt komponent. Det er også der, e2e-skiller sig fra unit tests. Unit tests kan tjekke små funktioner. E2e fortæller dig, om en rigtig bruger kan gennemføre noget meningsfuldt.
Sådan undgår du at drukne i timeouts og flaky tests
Det hurtigste sted at miste tålmodigheden med e2e-tests er, når det hele fejler tilfældigt. Nogle gange lykkes det. Andre gange fejler det. Typisk med en eller anden timeout-fejl.
Der er to ting, der har hjulpet mig meget:
– Brug Playwrights egne locators i stedet for waitForTimeout
– Brug de indbyggede forventninger, som selv venter (til en grænse)
Dårligt eksempel:
await page.goto('/');
await page.waitForTimeout(2000);
const title = await page.$('h1');
Hvis din app en dag er langsommere end 2 sekunder, fejler testen. Hvis den er hurtigere, spilder du tid. Brug i stedet locators og forventninger:
await page.goto('/');
const title = page.getByTestId('page-title');
await expect(title).toHaveText('Min todo app');
Her vil Playwright selv prøve at finde elementet igen og igen i nogle sekunder og først fejle, hvis det virkelig ikke dukker op.
Hvis du arbejder med netværkskald, kan du også bruge page.waitForLoadState('networkidle') efter navigation, men ofte er locators + forventninger nok i simple SPA’er.
En anden typisk fejl er at skrive tests, der afhænger af data, som var der “tilfældigt”. Hvis din todo-liste fx loader nogle default-items fra en API, og du sletter den første, kan du ikke altid regne med, hvad der står.
Mit trick er:
– Opret altid dine egne test-data i starten af testen, inden du tjekker på dem.
Det er grunden til, at jeg i todo-eksemplet først tilføjer “Køb kaffe” og derefter leder efter lige præcis den tekst.
Smoke test på din rigtige, deployede URL
Det, der for alvor gjorde Playwright nyttigt for mig, var, da jeg stoppede med kun at teste lokalt. Det er hyggeligt, at tingene virker på localhost. Det er bare ikke altid det samme som produktion.
Jeg laver tit en separat Playwright-config kun til prod. For eksempel:
// playwright.prod.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://min-todo-app.vercel.app',
},
reporter: 'html',
});
Så kan jeg køre mine tests direkte mod den URL:
npx playwright test --config=playwright.prod.config.ts
De samme tests, men nu klikker de rundt på den rigtige side. Det fanger fejl som:
– Environment-variabler, der ikke er sat i produktion
– Bygget, der fejlede, men hvor hosten stadig viser en gammel version
– API-URL, der peger på localhost i stedet for prod
Hvis du en dag får lyst, kan du smide det ind i CI, så det kører automatisk efter deploy. På GitHub Actions kan du fx lave et simpelt workflow, der:
– Kører, når du pusher til main
– Deploy’er din app til Netlify / Vercel
– Kører npx playwright test --config=playwright.prod.config.ts
Men du behøver ikke starte der. Jeg kørte mine smoke tests manuelt længe, især på små projekter:
npm run build
npm run deploy
npx playwright test --config=playwright.prod.config.ts
Det tager et minut, og så ved du, om du skal sende linket videre til andre mennesker med god samvittighed.
Fejlsøgning med screenshots, video og traces
Når en e2e-test fejler, er det nemt bare at bande og køre den igen i håbet om, at den “lige var uheldig”. Det virker sjældent i længden.
Playwright har tre ting, jeg bruger meget, når en test driller:
– Video af test-run
– Screenshots ved fejl
– Traces, hvor du kan gå klik for klik igennem testen
I din playwright.config kan du slå det til:
use: {
baseURL: 'http://localhost:5173',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-on-failure',
}
Når en test fejler, lægger Playwright filer i test-results-mappen. En typisk aften hos mig er:
– Jeg kører tests
– En fejler
– Jeg åbner HTML-rapporten med:
npx playwright show-report
Herfra kan du klikke dig ind på den fejlede test, se screenshot, se video, og åbne trace viewer. Trace viewer er især rar: den viser dig dommen over hvert klik, hvilket element den troede, den klikkede på, og hvilke fejl der dukkede op.
Hvis du gerne vil gå mere i dybden med debugging generelt, har jeg skrevet om debugging af JavaScript og console-fejl i en anden artikel på Coding Class, men Playwrights rapporter er et godt sted at starte i test-sammenhæng.
Næste skridt med Playwright i dine egne projekter
Hvis du har læst hertil, og du stadig synes, det her virker en anelse tungt, er min anbefaling faktisk ret simpel: start med bare én test.
Min typiske rækkefølge i et nyt lille projekt er:
– Sæt Playwright op og ret baseURL til
– Tilføj 2-3 data-testid-attributter på de vigtigste elementer
– Skriv én smoke test, der bare åbner forsiden og tjekker en tekst
– Først derefter de to andre tests (formular + CRUD)
Når du først har de tre tests kørende lokalt, er det ikke et stort skridt at kopiere config’en og pege den på din prod-URL. Pludselig har du en deploy smoke test, som du kan køre, hver gang du bygger noget nyt.
Hvis du også er nysgerrig på andre typer tests, kan du på et tidspunkt koble e2e sammen med unit tests eller integrationstests. Men selv hvis du aldrig kommer dertil, vil bare 3 Playwright-tests og en lille deploy smoke test allerede placere dig foran mange hobbyprojekter, der lever på ren held.
Jeg er nysgerrig på, hvornår du første gang opdager en fejl via en e2e-test, som du ellers ville have sendt videre til en rigtig bruger, og om det ændrer måden, du bygger dine små projekter på om et par år.









Send kommentar
Du skal være logget ind for at skrive en kommentar.