# Custom Locator Strategies - Playwright Helper
This document describes how to configure and use custom locator strategies in the CodeceptJS Playwright helper.
# Configuration
Custom locator strategies can be configured in your codecept.conf.js
file:
exports.config = {
helpers: {
Playwright: {
url: 'http://localhost:3000',
browser: 'chromium',
customLocatorStrategies: {
byRole: (selector, root) => {
return root.querySelector(`[role="${selector}"]`)
},
byTestId: (selector, root) => {
return root.querySelector(`[data-testid="${selector}"]`)
},
byDataQa: (selector, root) => {
const elements = root.querySelectorAll(`[data-qa="${selector}"]`)
return Array.from(elements) // Return array for multiple elements
},
byAriaLabel: (selector, root) => {
return root.querySelector(`[aria-label="${selector}"]`)
},
byPlaceholder: (selector, root) => {
return root.querySelector(`[placeholder="${selector}"]`)
},
},
},
},
}
# Usage
Once configured, custom locator strategies can be used with the same syntax as other locator types:
# Basic Usage
// Find and interact with elements
I.click({ byRole: 'button' })
I.fillField({ byTestId: 'username' }, 'john_doe')
I.see('Welcome', { byAriaLabel: 'greeting' })
I.seeElement({ byDataQa: 'navigation' })
# Advanced Usage
// Use with within() blocks
within({ byRole: 'form' }, () => {
I.fillField({ byTestId: 'email' }, '[email protected]')
I.click({ byRole: 'button' })
})
// Mix with standard locators
I.seeElement({ byRole: 'main' })
I.seeElement('#sidebar') // Standard CSS selector
I.seeElement({ xpath: '//div[@class="content"]' }) // Standard XPath
// Use with grabbing methods
const text = I.grabTextFrom({ byTestId: 'status' })
const value = I.grabValueFrom({ byPlaceholder: 'Enter email' })
// Use with waiting methods
I.waitForElement({ byRole: 'alert' }, 5)
I.waitForVisible({ byDataQa: 'loading-spinner' }, 3)
# Locator Function Requirements
Custom locator functions must follow these requirements:
# Function Signature
(selector, root) => HTMLElement | HTMLElement[] | null
- selector: The selector value passed to the locator
- root: The DOM element to search within (usually
document
or a parent element) - Return: Single element, array of elements, or null/undefined if not found
# Example Functions
customLocatorStrategies: {
// Single element selector
byRole: (selector, root) => {
return root.querySelector(`[role="${selector}"]`);
},
// Multiple elements selector (returns first for interactions)
byDataQa: (selector, root) => {
const elements = root.querySelectorAll(`[data-qa="${selector}"]`);
return Array.from(elements);
},
// Complex selector with validation
byCustomAttribute: (selector, root) => {
if (!selector) return null;
try {
return root.querySelector(`[data-custom="${selector}"]`);
} catch (error) {
console.warn('Invalid selector:', selector);
return null;
}
},
// Case-insensitive text search
byTextIgnoreCase: (selector, root) => {
const elements = Array.from(root.querySelectorAll('*'));
return elements.find(el =>
el.textContent &&
el.textContent.toLowerCase().includes(selector.toLowerCase())
);
}
}
# Error Handling
The framework provides graceful error handling:
# Undefined Strategies
// This will throw an error
I.click({ undefinedStrategy: 'value' })
// Error: Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".
# Malformed Functions
If a custom locator function throws an error, it will be caught and logged:
byBrokenLocator: (selector, root) => {
throw new Error('This locator is broken')
}
// Usage will log warning but not crash the test:
I.seeElement({ byBrokenLocator: 'test' }) // Logs warning, returns null
# Best Practices
# 1. Naming Conventions
Use descriptive names that clearly indicate what the locator does:
// Good
byRole: (selector, root) => root.querySelector(`[role="${selector}"]`),
byTestId: (selector, root) => root.querySelector(`[data-testid="${selector}"]`),
// Avoid
by1: (selector, root) => root.querySelector(`[role="${selector}"]`),
custom: (selector, root) => root.querySelector(`[data-testid="${selector}"]`),
# 2. Error Handling
Always include error handling in your custom functions:
byRole: (selector, root) => {
if (!selector || !root) return null
try {
return root.querySelector(`[role="${selector}"]`)
} catch (error) {
console.warn(`Error in byRole locator:`, error)
return null
}
}
# 3. Multiple Elements
For selectors that may return multiple elements, return an array:
byClass: (selector, root) => {
const elements = root.querySelectorAll(`.${selector}`)
return Array.from(elements) // Convert NodeList to Array
}
# 4. Performance
Keep locator functions simple and fast:
// Good - simple querySelector
byTestId: (selector, root) => root.querySelector(`[data-testid="${selector}"]`),
// Avoid - complex DOM traversal
byComplexSearch: (selector, root) => {
// Avoid complex searches that iterate through many elements
return Array.from(root.querySelectorAll('*'))
.find(el => /* complex condition */);
}
# Testing Custom Locators
# Unit Testing
Test your custom locator functions independently:
describe('Custom Locators', () => {
it('should find elements by role', () => {
const mockRoot = {
querySelector: sinon.stub().returns(mockElement),
}
const result = customLocatorStrategies.byRole('button', mockRoot)
expect(mockRoot.querySelector).to.have.been.calledWith('[role="button"]')
expect(result).to.equal(mockElement)
})
})
# Integration Testing
Create acceptance tests that verify the locators work with real DOM:
Scenario('should use custom locators', I => {
I.amOnPage('/test-page')
I.seeElement({ byRole: 'navigation' })
I.click({ byTestId: 'submit-button' })
I.see('Success', { byAriaLabel: 'status-message' })
})
# Migration from Other Helpers
If you're migrating from WebDriver helper that already supports custom locators, the syntax is identical:
// WebDriver and Playwright both support this syntax:
I.click({ byTestId: 'submit' })
I.fillField({ byRole: 'textbox' }, 'value')
# Troubleshooting
# Common Issues
Locator not recognized: Ensure the strategy is defined in
customLocatorStrategies
and is a function.Elements not found: Check that your locator function returns the correct element or null.
Multiple elements: If your function returns an array, interactions will use the first element.
Timing issues: Custom locators work with all waiting methods (
waitForElement
, etc.).
# Debug Mode
Enable debug mode to see locator resolution:
// In codecept.conf.js
exports.config = {
helpers: {
Playwright: {
// ... other config
},
},
plugins: {
stepByStepReport: {
enabled: true,
},
},
}
# Verbose Logging
Custom locator registration is logged when the helper starts:
Playwright: registering custom locator strategy: byRole
Playwright: registering custom locator strategy: byTestId