11.4 — Locators
7.2 called selectors “THE locator skill of the job” — today the job’s actual tool arrives, with an upgrade in philosophy: find elements the way a USER finds them — by role and visible name — and redesigns stop killing your suite.
You’ll watch the same page get restyled and see exactly which locators survive. This single lesson prevents more future misery than any other in the phase.
// the same button, three ways:
page.locator("#btn-a4f2"); // dev furniture
page.locator(".btn.btn-primary"); // dev furniture
page.getByRole("button",
{ name: "Add to cart" }); // user-facing ✓
await page.getByLabel("Email").fill("ada@shop.com");
await page.getByPlaceholder("Search…").fill("mug");
await expect(page.getByText("Free shipping"))
.toBeVisible();
// narrowing inside a list:
page.getByRole("listitem")
.filter({ hasText: "Blue mug" })
.getByRole("button", { name: "Add" });First, precision: a locator is a saved DESCRIPTION of how to find element(s) — “the button named Add to cart” — not the element itself. Creating one touches nothing and can’t fail; it’s a note, not a lookup.
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
The “name” in getByRole("button", { name }) is the accessible name — computed from visible text, or a label, or an aria-label attribute. It matches case-insensitively and tolerates surrounding whitespace, and accepts a regex (name: /add to cart/i) when text varies.
Where do you LEARN an element’s role without memorizing tables? Two tools: the trace/UI mode’s locator picker (11.14) literally writes the best locator for whatever you click — and npx playwright codegen URL records your clicks as locator code. Use them as teachers, then edit with the ladder in mind.
Locators compose beyond filter: .first(), .last(), .nth(i) exist (use sparingly — position is the weakest meaning), and page.getByRole(...).or(page.getByText(...)) tries alternatives. And getByText matches substrings by default — exact: true when “Log” must not match “Logout”.
Job note: teams that adopted user-facing locators report the same before/after — selector maintenance drops from a weekly tax to a rarity. When you inherit a CSS-heavy suite (you will), migrate opportunistically: every locator you touch, promote one rung. Six months later the suite is boring — the highest compliment a suite can earn.
⌨️ the redesign survival test
Model both locator strategies against two versions of the same page — before and after a redesign — and let the numbers show which strategy survives.
requirements:
- Model the pages:
v1is an array with one element{ role: "button", name: "Add to cart", cls: "btn-primary" };v2is the SAME button after a redesign — same role and name, butclsis now"cta-new". - Write
byClass(els, cls)andbyRole(els, role, name)— each filters and returns how MANY match (4.9 + length). - Print four labeled counts:
v1 byClass: 1,v1 byRole: 1,v2 byClass: 0,v2 byRole: 1— then the verdict linesurvivor: getByRole.
when you press RUN, the console must show exactly:
✏️ Quick check 1
A redesign renames every CSS class but changes no text or behavior. Which locator still finds the button: .btn-primary or getByRole("button", { name: "Add to cart" })?
✏️ Quick check 2
Your locator matches TWO buttons and you call .click(). What does Playwright do — click the first, or throw an error?
✏️ Quick check 3
Three product rows each have an "Add" button. You want Blue mug’s. What do you narrow by first — position (nth) or content (filter hasText)?
🗣️ Now teach it back
Explain to a friend what a locator actually is (description vs element, and why freshness matters), the redesign story that justifies user-facing locators, where “role” comes from, and the priority ladder.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.