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.
| Goal | CSTesting |
|---|---|
| By role (button, heading, etc.) | CSS or XPath: "button", "button[type=submit]", "//h3[.='Sign up']", "input[type=checkbox]" |
| By text | XPath: "//*[contains(.,'Welcome, John!')]" or "//button[contains(.,'Sign in')]" |
| By label | Use 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:
| Format | Example | Description |
|---|---|---|
| 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 aname(e.g."name=password"). - Use
id=and ensure the label hasfor="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 havefor/idassociation.
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
| Topic | CSTesting |
|---|---|
| Create locator | browser.locator(selector) |
| Selector types | CSS (default), XPath (//... or xpath=...), id=, name=, any attr=value |
| Multiple matches | Use .first(), .last(), or .nth(index) before actions |
| By role | CSS/XPath: "button", "input[type=checkbox]", "//h3[.='Sign up']" |
| By text | XPath: "//*[contains(.,'text')]", "//button[contains(.,'Sign in')]" |
| By label | name=, id=, or XPath //label[contains(.,'Label')]//input |
| By placeholder / alt / title / test id | [attr="value"] or attr=value |
| In frame | browser.frame(iframeSelector) then await frame.click(...) / frame.locator(...) |
| Count | await browser.evaluate("document.querySelectorAll('sel').length") + expect(n).to_be(...) |
| Strictness | Action on multiple matches throws; use .first() / .last() / .nth() or a unique selector |