
Leta upp kontroller så som användaren uppfattar dem: knappens roll och namn, fältets etikett. En ny CSS-klass ska då inte fälla testet. Ett ändrat knappnamn eller två knappar med samma namn ska däremot ge ett fel som pekar ut locatorn.
1. Bestäm vad testet får bero på
När en kodassistent byter komponentbibliotek eller städar i stilarna kan klassen .btn-primary försvinna medan knappen ser ut och fungerar som förut. Ett test som letar efter klassen faller då, fast användaren inte märker någon skillnad. Playwrights bästa praxis tar upp just det fallet: ändrar en designer klassen kan testet gå sönder. Locatordokumentationen avråder från CSS och XPath eftersom DOM-strukturen ofta ändras.
Alternativet är ett kontrakt i användarens termer. getByRole('button', { name: 'Prenumerera', exact: true }) betyder ”knappen som heter Prenumerera”. Enligt dokumentationen speglar getByRole hur användare och hjälpmedel uppfattar sidan, utifrån ARIA-roll, ARIA-attribut och tillgängligt namn. För formulärfält finns getByLabel, som hittar fältet via dess kopplade etikett. Byter någon knappens text har kontraktet ändrats på riktigt, och då ska testet säga till.
Kontraktet har en gräns. Dokumentationen säger uttryckligen att rollocatorer inte ersätter tillgänglighetsgranskningar och konformitetstester. Att testet hittar en knapp betyder inte att sidan fungerar med tangentbord; det provet finns i guiden om tangentbordstest av en AI-byggd webbapp.
2. Förbered övningsmappen
Skapa en tom mapp. Du behöver Node.js med npm och en installerad Google Chrome. Övningen laddar sidan med page.setContent, så ingen server, inloggning eller publik tjänst behövs. Versionen är låst till den vi körde.
npm init -y
npm install --save-dev @playwright/test@1.64.1
Spara konfigurationen som playwright.config.ts. Med channel: 'chrome' används den installerade Chrome i stället för en nedladdad webbläsare, och retries: 0 gör att ett fel inte döljs av en omkörning.
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
retries: 0,
use: { browserName: 'chromium', channel: 'chrome' }
});
3. Skriv sidan och testerna
Skapa mappen tests och spara blocket nedan som tests/locators.spec.ts. Funktionen sida bygger samma lilla HTML-sida i tre varianter: originalet, en version med ny klass och ett extra omslag runt knappen, och en version där knappen bytt namn. Sidan har två avsiktliga fällor: knappen Avprenumerera bredvid Prenumerera och två knappar som båda heter Välj.
import { test, expect, type Page } from '@playwright/test';
function sida({ knapp = 'Prenumerera', klass = 'btn-primary', omslag = false } = {}) {
const skicka = `<button type="submit" class="${klass}">${knapp}</button>`;
return `<!doctype html><html lang="sv"><meta charset="utf-8"><title>Prov</title>
<form>
<label for="epost">E-postadress</label>
<input id="epost" type="email">
${omslag ? `<div class="actions">${skicka}</div>` : skicka}
<button type="button">Avprenumerera</button>
</form>
<p role="status"></p>
<ul>
<li><h2>Grund</h2><button type="button">Välj</button></li>
<li><h2>Pro</h2><button type="button">Välj</button></li>
</ul>
<script>
const status = document.querySelector('[role="status"]');
document.querySelector('form').addEventListener('submit', event => {
event.preventDefault();
status.textContent = 'Tack, ' + document.querySelector('#epost').value + ' är anmäld.';
});
for (const item of document.querySelectorAll('li')) {
item.querySelector('button').addEventListener('click', () => {
status.textContent = 'Vald plan: ' + item.querySelector('h2').textContent;
});
}
</script></html>`;
}
async function anmal(page: Page) {
await page.getByLabel('E-postadress').fill('test@example.com');
await page.getByRole('button', { name: 'Prenumerera', exact: true }).click();
await expect(page.getByRole('status')).toHaveText('Tack, test@example.com är anmäld.');
}
test('roll och namn hittar knappen', async ({ page }) => {
await page.setContent(sida());
await anmal(page);
});
test('ny CSS och ett extra omslag stoppar inte testet', async ({ page }) => {
await page.setContent(sida({ klass: 'knapp knapp--stor', omslag: true }));
await expect(page.locator('.btn-primary')).toHaveCount(0);
await anmal(page);
});
test('rätt plan väljs inom sitt listobjekt', async ({ page }) => {
await page.setContent(sida());
const pro = page.getByRole('listitem').filter({ has: page.getByRole('heading', { name: 'Pro' }) });
await pro.getByRole('button', { name: 'Välj' }).click();
await expect(page.getByRole('status')).toHaveText('Vald plan: Pro');
});
test('dubbla knappnamn ger strict mode violation', async ({ page }) => {
await page.setContent(sida());
const fel = await page.getByRole('button', { name: 'Välj' }).click({ timeout: 2000 }).catch(e => e);
expect(String(fel)).toContain('strict mode violation');
});
test('namn utan exact matchar även Avprenumerera', async ({ page }) => {
await page.setContent(sida());
await expect(page.getByRole('button', { name: 'Prenumerera' })).toHaveCount(2);
await expect(page.getByRole('button', { name: 'Prenumerera', exact: true })).toHaveCount(1);
});
test('ändrat knappnamn ger timeout med locatorn i felet', async ({ page }) => {
await page.setContent(sida({ knapp: 'Gå med' }));
const fel = await page.getByRole('button', { name: 'Prenumerera', exact: true }).click({ timeout: 2000 }).catch(e => e);
expect(String(fel)).toContain('Timeout 2000ms exceeded');
});
Hjälpfunktionen anmal är själva kontraktet: fältet med etiketten E-postadress, knappen som heter exakt Prenumerera och en statusrad med ett bestämt facit. Det andra testet kör samma funktion mot den omstylade varianten. Först kontrollerar det att .btn-primary inte längre finns, så att du ser att en CSS-selektor hade missat knappen medan rollocatorn fortfarande träffar.
4. Avgränsa dubbla namn utan first()
Locatorer är strikta: en åtgärd kastar ett fel om fler än ett element matchar. Två Välj-knappar blir därför ett fel, inte något Playwright gissar sig förbi. Dokumentationen avråder från first(), last() och nth(), eftersom Playwright kan klicka på ett annat element än du avsåg när sidan ändras. Avgränsa i stället till den del av sidan som bär betydelsen. I testet om rätt plan filtreras listobjekten till det som innehåller rubriken Pro, och först därefter söks knappen.
Den andra fällan är mindre uppenbar. Utan exact matchas namnet skiftlägesokänsligt som en delsträng, enligt API-referensen för getByRole. ”Prenumerera” träffar därför även ”Avprenumerera”, vilket testet med två träffar visar. Med exact: true blir matchningen skiftlägeskänslig och gäller hela strängen, men blanksteg i början och slutet tas fortfarande bort.
5. Kör och läs facit
Kör följande kommando från övningsmappen.
npx playwright test --reporter=line
Facit är sex godkända tester. Två av dem fångar felet och kontrollerar meddelandet, så att du ser hur signalen ser ut utan att körningen blir röd.
| Test | Variant | Facit |
|---|---|---|
| Roll och namn | Original | Statusraden visar Tack, test@example.com är anmäld. |
| Ny CSS | Ny klass och omslag | .btn-primary ger 0 träffar; samma anmälan lyckas. |
| Rätt plan | Original | Statusraden visar Vald plan: Pro. |
| Dubbla knappnamn | Original | Felet innehåller strict mode violation. |
| Namn utan exact | Original | 2 träffar utan exact, 1 med exact. |
| Ändrat namn | Knappen heter Gå med | Felet innehåller Timeout 2000ms exceeded. |
När vi skrev ut felen kom strict-felet efter ungefär 0,2 sekunder. Det angav att locatorn resolved to 2 elements och listade båda knapparna med var sin avgränsad locator som förslag. Det ändrade namnet gav i stället TimeoutError: locator.click: Timeout 2000ms exceeded, och anropsloggen visade waiting for getByRole('button', { name: 'Prenumerera', exact: true }).
De sex testerna kördes lokalt den 30 oktober 2026 med Node.js 24.13.0, Playwright Test 1.64.1 och installerad Chrome på Windows. Samtliga passerade. Resultatet gäller den här fixturen och den körningen.
6. Skilj automatisk väntan från rätt data
Playwright väntar automatiskt före en åtgärd. Inför ett klick kontrolleras enligt dokumentationen om actionability bland annat att locatorn pekar ut exakt ett element som är synligt, stabilt, tar emot händelser och är aktiverat. Det är något annat än en fast paus: Playwright väntar på villkoren, inte på en bestämd tid. Men väntan säger bara att knappen gick att klicka. Om appen visade rätt resultat avgör påståendet efteråt, här toHaveText på statusraden. Det försöker igen tills villkoret uppfylls, som standard i högst 5 sekunder enligt dokumentationen om assertions.
Timeouten i övningen är kortad till 2 sekunder för att felet ska komma snabbt. Utan den har en åtgärd ingen egen standardgräns och stoppas först av testets timeout på 30 sekunder, enligt tabellen över timeouter. Ett omdöpt knappnamn kan alltså se ut som ett långsamt test i en vanlig körning.
7. Använd kontraktet när AI ändrar gränssnittet
När ett test faller efter en AI-ändring avgör du först om namnet ändrats avsiktligt. Är ”Gå med” den beställda nya texten uppdaterar du testet. Var ändringen inte beställd ber du assistenten återställa knapptexten och behålla testet. Be inte om en CSS-selektor eller first() bara för att få testet grönt. Vill du se vad sidan innehöll när ett test föll kan du öppna körningen i Playwright Trace Viewer. Vilka flöden som behöver test över huvud taget tar guiden om testning av AI-genererad kod upp.
FAQ
Vad är skillnaden mellan getByRole och en CSS-selektor?
getByRole letar efter elementets roll och tillgängliga namn, alltså det användaren uppfattar. En CSS-selektor beror på klasser och DOM-struktur, som kan ändras utan att användaren märker någon skillnad.
Varför ska jag inte använda first() när två knappar heter likadant?
Playwrights dokumentation avråder från first(), last() och nth() eftersom ett annat element kan klickas när sidan ändras. Avgränsa i stället till rätt del av sidan, till exempel listobjektet med rubriken Pro.
Matchar getByRole knappnamnet exakt?
Inte som standard. Utan exact matchas namnet skiftlägesokänsligt som en delsträng, så Prenumerera träffar även Avprenumerera. Med exact: true krävs hela strängen.
Räcker getByRole som tillgänglighetstest?
Nej. Playwrights dokumentation säger att rollocatorer inte ersätter tillgänglighetsgranskningar och konformitetstester.
Källor och prov
- Playwright: Locators – rekommenderade locatorer, getByRole, getByLabel, strikthet, filter och avrådan från CSS, XPath och first().
- Playwright: Best Practices – användarnära attribut och CSS-klasser som går sönder.
- Playwright: Page.getByRole – namnmatchning och exact.
- Playwright: Auto-waiting – kontroller före klick.
- Playwright: Assertions – omförsök och standardtimeout för påståenden.
- Playwright: Timeouts – test- och åtgärdstimeout.
Primärkällor kontrollerade 30 oktober 2026. Övningsfilerna kördes samma dag i en isolerad mapp med Node.js 24.13.0, Playwright Test 1.64.1 och installerad Chrome på Windows. Sidan, namnen och facit är AI-kreatörs egna exempel; ingen tillgänglighetsgranskning gjordes.