Locators
Introduction
Locators are how you find element(s) on the page. ICSTestingLocator (C:\CSTesting-DotNet) uses Playwright auto-waiting before ClickAsync, TypeAsync, etc. Create one with browser.Locator(selector) (PascalCase methods in C#). Short snippets below omit await; in tests write await browser.Locator(...).ClickAsync(). Use NUnit Assert.That instead of any assertThat API.
Each time a locator is used for an action, the element is resolved at that moment. If the DOM changes between calls (e.g. re-render), the next action uses the current matching element.
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):
browser.Locator("name=user").type("John");
browser.Locator("name=password").type("secret-password");
browser.Locator("//button[contains(.,'Sign in')]").First().click();
Assert.That(await browser.Locator("//*[contains(.,'Welcome, John!')]").IsVisibleAsync(), Is.True);
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.
browser.Locator("button").click(); // OK if exactly one button
browser.Locator("button").First().click(); // first match
browser.Locator("li").Nth(2).click(); // third item
browser.Locator("a").Last().click(); // last link
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":
browser.Locator("//button[contains(.,'Sign in')]").First().click();
Example – heading "Sign up", checkbox "Subscribe", button "Submit":
Assert.That(await browser.Locator("//h3[.='Sign up']").IsVisibleAsync(), Is.True);
await browser.Locator("//label[contains(.,'Subscribe')]//input").CheckAsync();
await browser.Locator("//button[contains(.,'Submit')]").First().ClickAsync();
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:
browser.Locator("name=password").type("secret");
// or by label text
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):
browser.Locator("[placeholder='name@example.com']").type("playwright@microsoft.com");
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':
Assert.That(await browser.Locator("//*[contains(.,'Welcome, John')]").IsVisibleAsync(), Is.True);
Assert.That(await browser.Locator("//span[.='Welcome, John']").IsVisibleAsync(), Is.True);
For a button or link with text, narrow the tag to avoid matching random divs:
browser.Locator("//button[contains(.,'Sign in')]").First().click();
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:
browser.Locator("img[alt='playwright logo']").click();
browser.Locator("alt=playwright logo").click();
When to use: For img or area elements with alt text.
Locate by title
Use the title attribute:
var issues = await browser.Locator("[title='Issues count']").GetTextContentAsync();
Assert.That(issues, Does.Contain("25 issues"));
await browser.Locator("title=Issues count").GetTextContentAsync();
Locate by test id
Use data-testid= (or any attr=value for a custom attribute):
browser.Locator("data-testid=directions").click();
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=.
browser.Locator("button").click();
browser.Locator("//button").click();
browser.Locator("css=button").click();
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
Get an ICSTestingFrame with await browser.FrameAsync(...), then use frame.Locator(...):
var frame = await browser.FrameAsync("iframe#my-frame");
await frame.Locator("button").ClickAsync();
await frame.TypeAsync("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:
browser.Locator("button").click(); // error if multiple buttons
OK – explicitly pick one:
browser.Locator("button").First().click();
browser.Locator("button").Nth(1).click();
browser.Locator("button").Last().click();
Count matches: use EvaluateAsync (there is no LocatorCount on the browser API):
var n = await browser.EvaluateAsync<int>(
"() => document.querySelectorAll('button').length");
Assert.That(n, Is.EqualTo(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 EvaluateAsync to count nodes, then Assert.That:
var count = await browser.EvaluateAsync<int>(
"() => document.querySelectorAll('li').length");
Assert.That(count, Is.EqualTo(3));
Get a specific item by text
Use XPath to find the list item that contains the text, then act on it:
browser.Locator("//li[contains(.,'orange')]").click();
Get by index (nth)
When order is the only way to distinguish items:
var secondItem = browser.Locator("li").Nth(1);
await secondItem.ClickAsync();
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:
Assert.That(await browser.Locator("//li[contains(.,'apple')]").IsVisibleAsync(), Is.True);
Assert.That(await browser.Locator("li").First().GetTextContentAsync(), Does.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:
browser.Locator("//li[contains(.,'Product 2')]//button[contains(.,'Add to cart')]").click();
Example – row with "Mary" and "Say goodbye" button (chaining conditions in XPath):
browser.Locator("//li[contains(.,'Mary') and .//button[contains(.,'Say goodbye')]]").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 | await browser.FrameAsync(iframeSelector) then frame.Locator(...) |
| Count | EvaluateAsync with querySelectorAll, or assert text on one locator |
| Strictness | Action on multiple matches throws; use .First() / .Last() / .Nth() or a unique selector |