Locators

Introduction

Locators in CSTesting for Python (C:\CSTesting-Python) come from browser.locator(selector) on BrowserApi (cstesting/browser.py). Playwright auto-waits before actions. All locator methods that touch the page are async—use await.

Each action re-resolves the element against the current DOM.

Quick guide

CSTesting uses selector strings (CSS, XPath, or attribute shortcuts) instead of separate methods like getByRole or getByText. The table below shows the recommended way to achieve the same goals.

GoalCSTesting
By role (button, heading, etc.)CSS or XPath: "button", "button[type=submit]", "//h3[.='Sign up']", "input[type=checkbox]"
By textXPath: "//*[contains(.,'Welcome, John!')]" or "//button[contains(.,'Sign in')]"
By labelUse name= (if input has name) or id= (and ensure <label for="id">). For "label text" use XPath: "//label[contains(.,'User Name')]/input" or "//input[@placeholder='User Name']"
By placeholder"[placeholder='name@example.com']" or "placeholder=name@example.com" (attr shortcut)
By alt text"img[alt='playwright logo']" or "alt=playwright logo" (attr shortcut)
By title"[title='Issues count']" or "title=Issues count"
By test id"data-testid=directions" or "data-testid=directions"[data-testid="directions"]

Example (login-style flow, async):

from cstesting import expect

await browser.locator("name=user").type("John")
await browser.locator("name=password").type("secret-password")
await browser.locator("//button[contains(.,'Sign in')]").first().click()
visible = await browser.locator("//*[contains(.,'Welcome, John!')]").first().is_visible()
expect(visible).to_be_truthy()

Selector formats

Create a locator with browser.locator(selector). The selector can be:

FormatExampleDescription
CSS"button", "#id", ".class", "button[type=submit]"Default. Use for tags, ids, classes, attributes.
XPath"//button", "xpath=//button"Use for text content, hierarchy, or complex conditions.
id"id=myId"Resolved to #myId (CSS).
name"name=submit"Resolved to [name="submit"] (CSS).
Any attribute"data-testid=foo", "placeholder=Email"Resolved to [attr="value"] (CSS).

When multiple elements match, use .first(), .last(), or .nth(index) before an action; otherwise the action throws.

await browser.locator("button").click()           # one match
await browser.locator("button").first().click()
await browser.locator("li").nth(2).click()
await browser.locator("a").last().click()

Locating elements

Locate by role

Use CSS or XPath to target elements by their tag or ARIA role:

  • Button: "button", "input[type=submit]", "[role='button']"
  • Heading: "h1", "h2", "//h3[.='Sign up']"
  • Checkbox: "input[type=checkbox]"
  • Link: "a", "a[href='/login']"

Example – button with text "Sign in":

await browser.locator("//button[contains(.,'Sign in')]").first().click()

Example – heading "Sign up", checkbox "Subscribe", button "Submit":

expect(await browser.locator("//h3[.='Sign up']").first().is_visible()).to_be_truthy()
await browser.locator("//label[contains(.,'Subscribe')]//input").check()
await browser.locator("//button[contains(.,'Submit')]").first().click()

When to use: Prefer role/tag-based selectors (e.g. button, input[type=checkbox]) plus text or attributes so tests align with how users see the page.

Locate by label

Form controls often have a <label>. In CSTesting you can:

  • Use name= if the input has a name (e.g. "name=password").
  • Use id= and ensure the label has for="id" (e.g. "id=username").
  • Use XPath to find the input by its label text: "//label[contains(.,'Password')]//input" or "//input[@id=//label[contains(.,'Password')]/@for]" if you have for/id association.

Example:

await browser.locator("name=password").type("secret")
await browser.locator("//label[contains(.,'Password')]//input").type("secret")

When to use: Use when locating form fields; prefer name= or id= when possible.

Locate by placeholder

Use the placeholder attribute (CSS or attribute shortcut):

await browser.locator("[placeholder='name@example.com']").type("user@example.com")
await browser.locator("placeholder=name@example.com").type("user@example.com")

When to use: When the field has no (reliable) label but has a placeholder.

Locate by text

Use XPath and contains(., 'text') or exact match .='text':

from cstesting import expect
expect(await browser.locator("//*[contains(.,'Welcome, John')]").first().is_visible()).to_be_truthy()
expect(await browser.locator("//span[.='Welcome, John']").first().is_visible()).to_be_truthy()

For a button or link with text, narrow the tag to avoid matching random divs:

await browser.locator("//button[contains(.,'Sign in')]").first().click()
await browser.locator("//a[contains(.,'Get Started')]").first().click()

When to use: For non-interactive elements (div, span, p) use text locators. For buttons/links, combine tag + text (e.g. //button[contains(.,'...')]).

Locate by alt text

Use CSS or attribute shortcut for images:

await browser.locator("img[alt='playwright logo']").click()
await browser.locator("alt=playwright logo").click()

When to use: For img or area elements with alt text.

Locate by title

Use the title attribute:

text = await browser.locator("[title='Issues count']").first().text_content()
expect(text).to_contain("25 issues")
await browser.locator("title=Issues count").text_content()

Locate by test id

Use data-testid= (or any attr=value for a custom attribute):

await browser.locator("data-testid=directions").click()
await browser.locator("[data-testid='directions']").click()

When to use: For a stable testing contract. Less user-facing than role/text; combine with role or text when the visible behavior matters.

Custom test id attribute: If your app uses e.g. data-pw instead of data-testid, use the attribute shortcut: "data-pw=directions"[data-pw="directions"].

Locate by CSS or XPath

Use browser.locator(selector) with a CSS selector or XPath. CSS is default; XPath is used when the string starts with // or xpath=.

await browser.locator("button").click()
await browser.locator("//button").click()
await browser.locator("button").click()  # css is default
await browser.locator("xpath=//button").click()

When to use: Prefer user-facing selectors (role/tag + text, label, placeholder, test id). Avoid long CSS/XPath chains tied to layout (e.g. #tsf > div:nth-child(2) > ...); they break when the DOM changes.

Locators inside frames

To interact with elements inside an iframe, get a browser scoped to the frame, then use locator on it:

frame = browser.frame("iframe#my-frame")
await frame.click("button")
await frame.type("name=user", "John")

See Frames for nested frames and more examples.

Multiple matches and strictness

Locators are strict: if an action (click, type, check, etc.) is performed and the selector matches more than one element, CSTesting throws. Use .first(), .last(), or .nth(index) to pick one.

Throws if more than one button:

await browser.locator("button").click()  # error if multiple buttons

OK – explicitly pick one:

await browser.locator("button").first().click()
await browser.locator("button").nth(1).click()
await browser.locator("button").last().click()

Count matching elements (no hasCount helper—use DOM or Playwright page):

from cstesting import expect
n = await browser.evaluate("document.querySelectorAll('button').length")
expect(n).to_be(3)

Use .first() / .last() / .nth() only when necessary; prefer a selector that uniquely identifies the element (e.g. text, test id, or more specific CSS/XPath).

Lists

Count items

Use evaluate or assert on a specific item:

n = await browser.evaluate("document.querySelectorAll('li').length")
expect(n).to_be(3)

Get a specific item by text

Use XPath to find the list item that contains the text, then act on it:

await browser.locator("//li[contains(.,'orange')]").first().click()

Get by index (nth)

When order is the only way to distinguish items:

second_item = browser.locator("li").nth(1)
await second_item.click()

Use .nth() with care: if the list order changes, the wrong element may be targeted. Prefer text or test id when possible.

Assert text in a list

Assert that a locator's text matches:

from cstesting import expect
expect(await browser.locator("//li[contains(.,'apple')]").first().is_visible()).to_be_truthy()
text = await browser.locator("li").first().text_content()
expect(text).to_contain("apple")

Filtering (workarounds)

CSTesting does not have filter(hasText) or filter(has(locator)). Use instead:

  • By text: XPath //element[contains(.,'text')] or a more specific selector.
  • By child/descendant: XPath e.g. //li[.//h3[.='Product 2']]//button[contains(.,'Add to cart')].
  • By index: .nth(index) when you must rely on position.

Example – "Product 2" "Add to cart" button:

await browser.locator("//li[contains(.,'Product 2')]//button[contains(.,'Add to cart')]").first().click()

Example – row with "Mary" and "Say goodbye" button (chaining conditions in XPath):

await browser.locator("//li[contains(.,'Mary') and .//button[contains(.,'Say goodbye')]]").first().click()

Summary

TopicCSTesting
Create locatorbrowser.locator(selector)
Selector typesCSS (default), XPath (//... or xpath=...), id=, name=, any attr=value
Multiple matchesUse .first(), .last(), or .nth(index) before actions
By roleCSS/XPath: "button", "input[type=checkbox]", "//h3[.='Sign up']"
By textXPath: "//*[contains(.,'text')]", "//button[contains(.,'Sign in')]"
By labelname=, id=, or XPath //label[contains(.,'Label')]//input
By placeholder / alt / title / test id[attr="value"] or attr=value
In framebrowser.frame(iframeSelector) then await frame.click(...) / frame.locator(...)
Countawait browser.evaluate("document.querySelectorAll('sel').length") + expect(n).to_be(...)
StrictnessAction on multiple matches throws; use .first() / .last() / .nth() or a unique selector