Coming Soon: getLocatorsByAI is an upcoming feature currently in development. The API described below is subject to change.
Writing reliable locators is often painful—users must inspect HTML, guess selectors, and tweak through trial and error. This becomes especially hard when dealing with dynamic lists or repeating elements.
getLocatorsByAI(prompt: string) lets you describe what you want in natural language (e.g., “all product cards in the carousel”) and automatically returns stable locators. It caches results so most runs are as fast and deterministic as code, only invoking AI when the DOM changes.
Why not vanilla Playwright? Low-level page.locator() requires precise selectors that break when the DOM changes. Fully agentic agent.act() is powerful but heavyweight for simple element selection. getLocatorsByAI() bridges the gap—giving you AI-assisted, self-healing locators that feel native to Playwright.
Built-in Autofix: The Key Advantage
The biggest pain point with traditional Playwright locators is maintenance. When developers refactor the UI, selectors break and tests fail—even though the functionality still works. With getLocatorsByAI, your tests automatically adapt to DOM changes because they target semantic intent, not brittle selectors.
Traditional Playwright
With getLocatorsByAI
// ❌ Fragile: breaks when class names or structure change
test('add item to cart', async ({ page }) => {
await page.goto('/products');
// These selectors WILL break when:
// - Designer renames .product-card to .item-card
// - Dev wraps cards in a new container div
// - Class becomes .ProductCard (React component)
await page.locator('.product-grid .product-card').first().click();
await page.locator('button.add-to-cart-btn').click();
// Test fails → Manual fix required → Repeat forever
});
// ✅ Resilient: adapts automatically to DOM changes
test('add item to cart', async ({ page }) => {
await page.goto('/products');
// Describes WHAT you want, not WHERE it is
const { locator: products } = await getLocatorsByAI(
page,
'product cards in the product listing'
);
await products.first().click();
const { locator: addBtn } = await getLocatorsByAI(
page,
'the Add to Cart button'
);
await addBtn.click();
// Class renamed? Structure changed? Still works.
});
Real-World Autofix Examples
Here are concrete scenarios where getLocatorsByAI saves you from test maintenance:
Example 1: CSS Class Refactoring
Your design system team renames classes from .btn-primary to .Button--primary:
// ❌ Traditional: BREAKS
await page.locator('.btn-primary').click();
// Error: locator('.btn-primary') resolved to 0 elements
// ✅ AI Locator: WORKS
const { locator } = await getLocatorsByAI(page, 'the primary action button');
await locator.click();
// Still finds it via aria-label or role
Example 2: Component Library Migration
You migrate from Bootstrap to a custom component library, changing the entire DOM structure:
// Before: <button class="btn btn-success" id="submit-form">Submit</button>
// After: <CustomButton variant="success" data-action="submit">Submit</CustomButton>
// ❌ Traditional: BREAKS
await page.locator('#submit-form').click();
await page.locator('.btn-success').click();
// Both fail - ID removed, classes changed
// ✅ AI Locator: WORKS
const { locator } = await getLocatorsByAI(page, 'the Submit button');
await locator.click();
// Finds by button text/label regardless of implementation
Example 3: Layout Restructuring
A product grid gets wrapped in new container divs for a redesign:
// Before: <div class="products"><div class="card">...</div></div>
// After: <section class="product-section"><div class="grid-container"><article class="product-item">...</article></div></section>
// ❌ Traditional: BREAKS
await page.locator('.products .card').first().click();
// Error: strict mode violation or 0 elements
// ✅ AI Locator: WORKS
const { locator } = await getLocatorsByAI(page, 'the first product card');
await locator.click();
// Semantic understanding survives structural changes
Example 4: Dynamic List Items
An e-commerce site changes how cart items are rendered:
// ❌ Traditional: Fragile nth-child selectors
await page.locator('.cart-items li:nth-child(3) .remove-btn').click();
// Breaks if list order changes or wrapper elements are added
// ✅ AI Locator: Robust
const { locator } = await getLocatorsByAI(
page,
'the remove button for "Blue T-Shirt" in the cart'
);
await locator.click();
// Finds by product context, not DOM position
Why does this work? getLocatorsByAI uses the page’s accessibility tree (aria snapshot) to understand elements semantically. As long as your UI remains accessible, the AI can locate elements by their meaning rather than their implementation details.
Installation
Install the Stably Playwright test integration and import it in place of the Playwright test runner:
import { test, expect, getLocatorsByAI } from '@stablyai/playwright-test';
The API surface stays the same as Playwright, but you gain access to AI-powered locator generation.
getLocatorsByAI
Use prompt-based locator generation to find elements without writing fragile selectors.
Basic Usage
import { test, expect, getLocatorsByAI } from '@stablyai/playwright-test';
test('interact with product cards', async ({ page }) => {
await page.goto('/products');
const { locator, count, reasoning } = await getLocatorsByAI(
page,
'all product cards in the featured section'
);
// Use like any Playwright locator
await expect(locator).toHaveCount(count);
// Get all matching elements
const cards = await locator.all();
await cards[0].click();
});
How It Works
getLocatorsByAI performs intelligent element location in three steps:
- Aria Snapshot: Captures the page’s accessibility tree to understand the semantic structure of the DOM.
- AI Analysis: Evaluates your prompt against the aria snapshot to identify matching elements.
- Locator Generation: Returns Playwright-native locators that can be used like any standard locator.
Important: getLocatorsByAI locates elements based on their aria labels and semantic structure, not visual appearance. Prompts like “blue circle buttons” or “the large header” will not work reliably. Instead, describe elements by their role, label, or content (e.g., “Add to Cart buttons” or “the navigation menu”).
Method Signature
await getLocatorsByAI(
page: Page,
prompt: string,
options?: GetLocatorsByAIOptions
): Promise<GetLocatorsByAIResult>
Parameters:
page - The Playwright Page instance
prompt - Natural language description of the element(s) to locate
options - Optional configuration (see Options below)
Returns:
interface GetLocatorsByAIResult {
locator: Locator | FrameLocator; // Playwright locator for matched elements
count: number; // Number of elements found
reasoning: string; // AI's explanation of how it found the elements
}
The returned locator behaves exactly like Playwright’s native Locator object. Call .all() to get an array of individual locators for each matched element.
Common Use Cases
Locate Dynamic Lists
const { locator } = await getLocatorsByAI(
page,
'all items in the shopping cart'
);
const items = await locator.all();
Find Specific Elements by Role
const { locator } = await getLocatorsByAI(
page,
'the submit button in the checkout form'
);
await locator.click();
Target Elements by Position
const { locator } = await getLocatorsByAI(
page,
'the third row in the users table'
);
await expect(locator).toBeVisible();
Locate Elements in Frames
const { locator } = await getLocatorsByAI(
page,
'the email input field in the newsletter signup iframe'
);
await locator.fill('[email protected]');
Options
interface GetLocatorsByAIOptions {
cache?: boolean | {
name: string;
};
}
Caching:
cache: true (default) - Caches the AI result using an auto-generated key
cache: false - Disables caching; AI is invoked on every call
cache: { name: 'my-cache-key' } - Uses a custom cache key for fine-grained control
// Default: caching enabled
const { locator } = await getLocatorsByAI(page, 'the main navigation menu');
// Disable caching for dynamic content
const { locator } = await getLocatorsByAI(
page,
'the current user badge',
{ cache: false }
);
// Custom cache key
const { locator } = await getLocatorsByAI(
page,
'product cards on the homepage',
{ cache: { name: 'homepage-products' } }
);
Caching significantly improves performance. When cached, the SDK only invokes AI if the page’s aria snapshot has changed since the last call. For stable pages, subsequent runs complete instantly with deterministic results.
Best Practices
Write Semantic Prompts
Describe elements by their accessible name, role, or text content—not visual appearance.
// ❌ Visual descriptions don't work
await getLocatorsByAI(page, 'the blue rounded buttons');
await getLocatorsByAI(page, 'the large bold header');
// ✅ Semantic descriptions work
await getLocatorsByAI(page, 'buttons labeled "Add to Cart"');
await getLocatorsByAI(page, 'the main heading with text "Welcome"');
One Locator Type Per Prompt
While you can request multiple unrelated locators in a single prompt (e.g., “get all the circle buttons and the first two headers”), this is discouraged. Playwright’s strictness model recommends locators that uniquely identify target elements. Mixing unrelated elements in one locator can lead to unexpected behavior when the page changes.
// ❌ Discouraged: multiple unrelated element types
const { locator } = await getLocatorsByAI(
page,
'all the add buttons and the page title'
);
// ✅ Recommended: separate calls for different element types
const { locator: addButtons } = await getLocatorsByAI(page, 'all Add to Cart buttons');
const { locator: title } = await getLocatorsByAI(page, 'the page title');
Use the Reasoning Output
The reasoning field explains how the AI interpreted your prompt. Use it for debugging when locators don’t match as expected.
const { locator, count, reasoning } = await getLocatorsByAI(
page,
'all product cards'
);
console.log(`Found ${count} elements`);
console.log(`AI reasoning: ${reasoning}`);
For elements that don’t change between test runs, caching makes getLocatorsByAI as fast as hardcoded selectors.
// Cache stable navigation elements
const { locator: nav } = await getLocatorsByAI(
page,
'the main navigation links',
{ cache: { name: 'main-nav' } }
);
// Disable cache for user-specific content
const { locator: userMenu } = await getLocatorsByAI(
page,
'the current user dropdown',
{ cache: false }
);
Troubleshooting
No elements found
The AI couldn’t locate elements matching your description. Refine your prompt to use semantic identifiers.
// ❌ Too vague or visual
const { count } = await getLocatorsByAI(page, 'the buttons');
// count: 0 (or unexpected results)
// ✅ More specific and semantic
const { count } = await getLocatorsByAI(page, 'buttons with role "button" in the form');
Too many elements matched
The prompt is matching more elements than intended. Add constraints to narrow the selection.
// ❌ Matches all links on the page
const { locator } = await getLocatorsByAI(page, 'links');
// ✅ Scoped to specific section
const { locator } = await getLocatorsByAI(page, 'links in the footer navigation');
Locator stopped working after page change
If caching is enabled and the DOM structure changed significantly, the cached locator may be stale. Either:
- Let the cache auto-invalidate (happens when aria snapshot changes)
- Use a unique cache name per page version
- Disable caching for highly dynamic elements
const { locator } = await getLocatorsByAI(
page,
'the dynamic content area',
{ cache: false } // Always get fresh locator
);
When to Use
-
Use
getLocatorsByAI when:
- You need to locate elements in dynamic lists or repeated structures
- Writing precise selectors is difficult due to complex or changing DOM
- You want self-healing locators that adapt to minor DOM changes
- You’re prototyping tests and want to describe elements naturally
-
Use standard Playwright locators when:
- You have stable, unique selectors (IDs, data-testid attributes)
- Performance is critical and AI overhead is not acceptable
- The element structure is simple and unlikely to change
References