FOUNDATIONS · LESSON 3 OF 24
Choose resilient locators
Find controls by user-facing meaning and explicit test contracts.
- getByRole
- getByLabel
- test ids
- strictness
Your runnable checkpoint
Extract into a new practice folder. Each download contains tests, configuration, the complete local practice app and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.
Install dependencies and browser binaries using the commands below. The test runner starts the practice server on port 4174. Tests target localhost, never the production EduPoly site.
1. Understand the concepts
A locator is a description of how to find an element, not a permanently cached DOM node. Playwright resolves it again for actions and assertions, helping tests survive rerenders.
Prefer roles and accessible names for interactive controls, and labels for inputs. Accessible names can come from text, labels or aria-label. A test id is useful for a deliberate testing contract when semantics are not enough.
Actions usually require one matching element. If a locator matches several buttons, narrow it with a container or a more precise name. Do not hide accidental ambiguity with first() unless first is actually part of the requirement.
Long CSS ancestry chains and generated class names couple tests to layout details. CSS is still useful for a stable attribute or a deliberate implementation check, but prefer a user-facing assertion when testing behavior.
2. Build and run
These are complete test files and configuration. The download also includes app/ and server.mjs; preserve their paths. Stop any manually started practice server before running the suite.
npm install
npx playwright install chromium
npm run check
npm testtests/lesson.spec.ts
import { test, expect } from '@playwright/test';
test('find the task controls by meaning', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Task title', { exact: true }).fill('Learn locators');
await page.getByRole('button', { name: 'Add task', exact: true }).click();
const row = page.getByTestId('task-row').filter({ hasText: 'Learn locators' });
await expect(row).toHaveCount(1);
await expect(row.getByRole('button', { name: 'Complete', exact: true })).toBeVisible();
});
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined,
timeout: 30000,
expect: { timeout: 5000 },
reporter: [['list'], ['html', { open: 'never' }]],
use: { baseURL: 'http://127.0.0.1:4174', trace: 'retain-on-failure', screenshot: 'only-on-failure' },
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: { command: 'node server.mjs', url: 'http://127.0.0.1:4174', reuseExistingServer: false, timeout: 15000 }
});
package.json
{
"name": "edupoly-playwright-locators-and-accessible-names",
"version": "1.0.0",
"private": true,
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:ui": "playwright test --ui",
"check": "tsc --noEmit",
"report": "playwright show-report",
"serve": "node server.mjs"
},
"devDependencies": {
"@playwright/test": "1.56.1",
"typescript": "5.9.3",
"@types/node": "22.20.5"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": [
"node"
],
"noEmit": true
},
"include": [
"tests/**/*.ts",
"pages/**/*.ts",
"playwright.config.ts"
]
}
.gitignore
node_modules/
playwright-report/
test-results/
playwright/.auth/
.env
3. Trace the code and check the result
- getByLabel follows the form label rather than its CSS position.
- filter narrows repeated rows by their content.
- The scoped button locator avoids other rows’ Complete buttons.
Verify your result
- One matching task row appears.
- The Complete button is found inside that row.
4. Practice independently
Create two tasks and locate only the second task’s Delete button.
Show the explanation
Use getByTestId("task-row").filter({ hasText: "Second task" }).getByRole("button", { name: "Delete", exact: true }). Assert the first task remains afterwards.
When something goes wrong
If a browser executable is missing, run npx playwright install chromium in the project folder. If port 4174 is occupied, stop your manual practice server. Read assertion call logs for mismatched locators or expectations. Use npm run report to inspect failures; do not add fixed sleeps or broad resets to hide them.
