JS Sketchbooksee JavaScript think ✏️
← back to phase 11

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.

watch it happen
// 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 DESCRIPTION“button named Add to cart”→ resolved fresh →today’s page( Add to cart )a locator is a saved DESCRIPTION of how to find something — not the elementitself
under the hood

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.

your turn

⌨️ 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: v1 is an array with one element { role: "button", name: "Add to cart", cls: "btn-primary" }; v2 is the SAME button after a redesign — same role and name, but cls is now "cta-new".
  • Write byClass(els, cls) and byRole(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 line survivor: getByRole.

when you press RUN, the console must show exactly:

v1 byClass: 1
v1 byRole: 1
v2 byClass: 0
v2 byRole: 1
survivor: getByRole

✏️ 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)?

teach it back

🗣️ 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.

a few sentences, minimum — you’ve got this
to remember
A locator = a saved DESCRIPTION, re-resolved fresh at every use (never a stale element) — the freshness that makes 11.6’s retrying possible.
The redesign test: CSS (developer furniture) dies on restyles; getByRole(name) survives — role/name come from the accessibility tree (7.1’s tags, mapped), so good locators = accessible app.
Ladder: role > label > text > testId > CSS (reach down reluctantly). Strict mode: 2 matches = loud error. Lists: chain + filter({ hasText }) — content beats position.
next: 11.5 Actions →