# 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 documentor 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 - customLocatorStrategiesand 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