/**
* Utilities for finding page containers by visible text
*/
import { Page, Locator } from 'playwright';
export interface ContainerLocation {
locator: Locator;
selector: string;
}
/**
* Find a container element by visible text using multiple strategies
*
* Strategies (in order of precedence):
* 1. Find by heading containing text
* 2. Find by aria-label or aria-labelledby attributes
* 3. Find by role with matching name
* 4. Find any structural element containing the text
*/
export async function findContainerByText(
page: Page,
containerText: string
): Promise<ContainerLocation> {
let containerLocator: Locator | null = null;
// Strategy 1: Find by heading containing text
const heading = page.locator('h1, h2, h3, h4, h5, h6').filter({ hasText: containerText });
if (await heading.count() > 0) {
// Get parent section/div/article
containerLocator = heading.first().locator('xpath=ancestor::*[self::section or self::div or self::article or self::nav or self::aside][1]');
if (await containerLocator.count() === 0) {
containerLocator = heading.first().locator('xpath=..');
}
}
// Strategy 2: Find section/div with aria-label or text content
if (!containerLocator || await containerLocator.count() === 0) {
containerLocator = page.locator(`[aria-label*="${containerText}" i], [aria-labelledby*="${containerText}" i]`);
}
// Strategy 3: Find by role with name
if (!containerLocator || await containerLocator.count() === 0) {
containerLocator = page.getByRole('region', { name: new RegExp(containerText, 'i') });
}
// Strategy 4: Find any element containing the text
if (!containerLocator || await containerLocator.count() === 0) {
containerLocator = page
.locator('section, div, nav, aside, article')
.filter({ hasText: containerText })
.first();
}
if (!containerLocator || await containerLocator.count() === 0) {
throw new Error(
`Could not find container with text "${containerText}". ` +
'Try being more specific or use a different part of the visible text.'
);
}
// Get a stable selector for axe
const selector = await getStableSelector(containerLocator);
return {
locator: containerLocator,
selector
};
}
/**
* Generate a stable CSS selector from a locator
* Tries: ID -> Class -> Tag name
*/
async function getStableSelector(locator: Locator): Promise<string> {
try {
const element = await locator.first().elementHandle();
if (!element) {
return 'body';
}
const tagName = await element.evaluate(el => el.tagName.toLowerCase());
const id = await element.evaluate(el => el.id);
const className = await element.evaluate(el => el.className);
if (id) {
return `#${id}`;
}
if (className && typeof className === 'string') {
const firstClass = className.split(' ')[0];
if (firstClass) {
return `.${firstClass}`;
}
}
return tagName;
} catch (e) {
return 'body'; // fallback
}
}