|  | 
|  | 1 | +# Custom Locator Strategies - Playwright Helper | 
|  | 2 | + | 
|  | 3 | +This document describes how to configure and use custom locator strategies in the CodeceptJS Playwright helper. | 
|  | 4 | + | 
|  | 5 | +## Configuration | 
|  | 6 | + | 
|  | 7 | +Custom locator strategies can be configured in your `codecept.conf.js` file: | 
|  | 8 | + | 
|  | 9 | +```js | 
|  | 10 | +exports.config = { | 
|  | 11 | +  helpers: { | 
|  | 12 | +    Playwright: { | 
|  | 13 | +      url: 'http://localhost:3000', | 
|  | 14 | +      browser: 'chromium', | 
|  | 15 | +      customLocatorStrategies: { | 
|  | 16 | +        byRole: (selector, root) => { | 
|  | 17 | +          return root.querySelector(`[role="${selector}"]`) | 
|  | 18 | +        }, | 
|  | 19 | +        byTestId: (selector, root) => { | 
|  | 20 | +          return root.querySelector(`[data-testid="${selector}"]`) | 
|  | 21 | +        }, | 
|  | 22 | +        byDataQa: (selector, root) => { | 
|  | 23 | +          const elements = root.querySelectorAll(`[data-qa="${selector}"]`) | 
|  | 24 | +          return Array.from(elements) // Return array for multiple elements | 
|  | 25 | +        }, | 
|  | 26 | +        byAriaLabel: (selector, root) => { | 
|  | 27 | +          return root.querySelector(`[aria-label="${selector}"]`) | 
|  | 28 | +        }, | 
|  | 29 | +        byPlaceholder: (selector, root) => { | 
|  | 30 | +          return root.querySelector(`[placeholder="${selector}"]`) | 
|  | 31 | +        }, | 
|  | 32 | +      }, | 
|  | 33 | +    }, | 
|  | 34 | +  }, | 
|  | 35 | +} | 
|  | 36 | +``` | 
|  | 37 | + | 
|  | 38 | +## Usage | 
|  | 39 | + | 
|  | 40 | +Once configured, custom locator strategies can be used with the same syntax as other locator types: | 
|  | 41 | + | 
|  | 42 | +### Basic Usage | 
|  | 43 | + | 
|  | 44 | +```js | 
|  | 45 | +// Find and interact with elements | 
|  | 46 | +I.click({ byRole: 'button' }) | 
|  | 47 | +I.fillField({ byTestId: 'username' }, 'john_doe') | 
|  | 48 | +I.see('Welcome', { byAriaLabel: 'greeting' }) | 
|  | 49 | +I.seeElement({ byDataQa: 'navigation' }) | 
|  | 50 | +``` | 
|  | 51 | + | 
|  | 52 | +### Advanced Usage | 
|  | 53 | + | 
|  | 54 | +```js | 
|  | 55 | +// Use with within() blocks | 
|  | 56 | +within({ byRole: 'form' }, () => { | 
|  | 57 | +  I.fillField({ byTestId: 'email' }, 'test@example.com') | 
|  | 58 | +  I.click({ byRole: 'button' }) | 
|  | 59 | +}) | 
|  | 60 | + | 
|  | 61 | +// Mix with standard locators | 
|  | 62 | +I.seeElement({ byRole: 'main' }) | 
|  | 63 | +I.seeElement('#sidebar') // Standard CSS selector | 
|  | 64 | +I.seeElement({ xpath: '//div[@class="content"]' }) // Standard XPath | 
|  | 65 | + | 
|  | 66 | +// Use with grabbing methods | 
|  | 67 | +const text = I.grabTextFrom({ byTestId: 'status' }) | 
|  | 68 | +const value = I.grabValueFrom({ byPlaceholder: 'Enter email' }) | 
|  | 69 | + | 
|  | 70 | +// Use with waiting methods | 
|  | 71 | +I.waitForElement({ byRole: 'alert' }, 5) | 
|  | 72 | +I.waitForVisible({ byDataQa: 'loading-spinner' }, 3) | 
|  | 73 | +``` | 
|  | 74 | + | 
|  | 75 | +## Locator Function Requirements | 
|  | 76 | + | 
|  | 77 | +Custom locator functions must follow these requirements: | 
|  | 78 | + | 
|  | 79 | +### Function Signature | 
|  | 80 | + | 
|  | 81 | +```js | 
|  | 82 | +(selector, root) => HTMLElement | HTMLElement[] | null | 
|  | 83 | +``` | 
|  | 84 | + | 
|  | 85 | +- **selector**: The selector value passed to the locator | 
|  | 86 | +- **root**: The DOM element to search within (usually `document` or a parent element) | 
|  | 87 | +- **Return**: Single element, array of elements, or null/undefined if not found | 
|  | 88 | + | 
|  | 89 | +### Example Functions | 
|  | 90 | + | 
|  | 91 | +```js | 
|  | 92 | +customLocatorStrategies: { | 
|  | 93 | +  // Single element selector | 
|  | 94 | +  byRole: (selector, root) => { | 
|  | 95 | +    return root.querySelector(`[role="${selector}"]`); | 
|  | 96 | +  }, | 
|  | 97 | + | 
|  | 98 | +  // Multiple elements selector (returns first for interactions) | 
|  | 99 | +  byDataQa: (selector, root) => { | 
|  | 100 | +    const elements = root.querySelectorAll(`[data-qa="${selector}"]`); | 
|  | 101 | +    return Array.from(elements); | 
|  | 102 | +  }, | 
|  | 103 | + | 
|  | 104 | +  // Complex selector with validation | 
|  | 105 | +  byCustomAttribute: (selector, root) => { | 
|  | 106 | +    if (!selector) return null; | 
|  | 107 | +    try { | 
|  | 108 | +      return root.querySelector(`[data-custom="${selector}"]`); | 
|  | 109 | +    } catch (error) { | 
|  | 110 | +      console.warn('Invalid selector:', selector); | 
|  | 111 | +      return null; | 
|  | 112 | +    } | 
|  | 113 | +  }, | 
|  | 114 | + | 
|  | 115 | +  // Case-insensitive text search | 
|  | 116 | +  byTextIgnoreCase: (selector, root) => { | 
|  | 117 | +    const elements = Array.from(root.querySelectorAll('*')); | 
|  | 118 | +    return elements.find(el => | 
|  | 119 | +      el.textContent && | 
|  | 120 | +      el.textContent.toLowerCase().includes(selector.toLowerCase()) | 
|  | 121 | +    ); | 
|  | 122 | +  } | 
|  | 123 | +} | 
|  | 124 | +``` | 
|  | 125 | + | 
|  | 126 | +## Error Handling | 
|  | 127 | + | 
|  | 128 | +The framework provides graceful error handling: | 
|  | 129 | + | 
|  | 130 | +### Undefined Strategies | 
|  | 131 | + | 
|  | 132 | +```js | 
|  | 133 | +// This will throw an error | 
|  | 134 | +I.click({ undefinedStrategy: 'value' }) | 
|  | 135 | +// Error: Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function". | 
|  | 136 | +``` | 
|  | 137 | + | 
|  | 138 | +### Malformed Functions | 
|  | 139 | + | 
|  | 140 | +If a custom locator function throws an error, it will be caught and logged: | 
|  | 141 | + | 
|  | 142 | +```js | 
|  | 143 | +byBrokenLocator: (selector, root) => { | 
|  | 144 | +  throw new Error('This locator is broken') | 
|  | 145 | +} | 
|  | 146 | + | 
|  | 147 | +// Usage will log warning but not crash the test: | 
|  | 148 | +I.seeElement({ byBrokenLocator: 'test' }) // Logs warning, returns null | 
|  | 149 | +``` | 
|  | 150 | + | 
|  | 151 | +## Best Practices | 
|  | 152 | + | 
|  | 153 | +### 1. Naming Conventions | 
|  | 154 | + | 
|  | 155 | +Use descriptive names that clearly indicate what the locator does: | 
|  | 156 | + | 
|  | 157 | +```js | 
|  | 158 | +// Good | 
|  | 159 | +byRole: (selector, root) => root.querySelector(`[role="${selector}"]`), | 
|  | 160 | +byTestId: (selector, root) => root.querySelector(`[data-testid="${selector}"]`), | 
|  | 161 | + | 
|  | 162 | +// Avoid | 
|  | 163 | +by1: (selector, root) => root.querySelector(`[role="${selector}"]`), | 
|  | 164 | +custom: (selector, root) => root.querySelector(`[data-testid="${selector}"]`), | 
|  | 165 | +``` | 
|  | 166 | + | 
|  | 167 | +### 2. Error Handling | 
|  | 168 | + | 
|  | 169 | +Always include error handling in your custom functions: | 
|  | 170 | + | 
|  | 171 | +```js | 
|  | 172 | +byRole: (selector, root) => { | 
|  | 173 | +  if (!selector || !root) return null | 
|  | 174 | +  try { | 
|  | 175 | +    return root.querySelector(`[role="${selector}"]`) | 
|  | 176 | +  } catch (error) { | 
|  | 177 | +    console.warn(`Error in byRole locator:`, error) | 
|  | 178 | +    return null | 
|  | 179 | +  } | 
|  | 180 | +} | 
|  | 181 | +``` | 
|  | 182 | + | 
|  | 183 | +### 3. Multiple Elements | 
|  | 184 | + | 
|  | 185 | +For selectors that may return multiple elements, return an array: | 
|  | 186 | + | 
|  | 187 | +```js | 
|  | 188 | +byClass: (selector, root) => { | 
|  | 189 | +  const elements = root.querySelectorAll(`.${selector}`) | 
|  | 190 | +  return Array.from(elements) // Convert NodeList to Array | 
|  | 191 | +} | 
|  | 192 | +``` | 
|  | 193 | + | 
|  | 194 | +### 4. Performance | 
|  | 195 | + | 
|  | 196 | +Keep locator functions simple and fast: | 
|  | 197 | + | 
|  | 198 | +```js | 
|  | 199 | +// Good - simple querySelector | 
|  | 200 | +byTestId: (selector, root) => root.querySelector(`[data-testid="${selector}"]`), | 
|  | 201 | + | 
|  | 202 | +// Avoid - complex DOM traversal | 
|  | 203 | +byComplexSearch: (selector, root) => { | 
|  | 204 | +  // Avoid complex searches that iterate through many elements | 
|  | 205 | +  return Array.from(root.querySelectorAll('*')) | 
|  | 206 | +    .find(el => /* complex condition */); | 
|  | 207 | +} | 
|  | 208 | +``` | 
|  | 209 | + | 
|  | 210 | +## Testing Custom Locators | 
|  | 211 | + | 
|  | 212 | +### Unit Testing | 
|  | 213 | + | 
|  | 214 | +Test your custom locator functions independently: | 
|  | 215 | + | 
|  | 216 | +```js | 
|  | 217 | +describe('Custom Locators', () => { | 
|  | 218 | +  it('should find elements by role', () => { | 
|  | 219 | +    const mockRoot = { | 
|  | 220 | +      querySelector: sinon.stub().returns(mockElement), | 
|  | 221 | +    } | 
|  | 222 | + | 
|  | 223 | +    const result = customLocatorStrategies.byRole('button', mockRoot) | 
|  | 224 | +    expect(mockRoot.querySelector).to.have.been.calledWith('[role="button"]') | 
|  | 225 | +    expect(result).to.equal(mockElement) | 
|  | 226 | +  }) | 
|  | 227 | +}) | 
|  | 228 | +``` | 
|  | 229 | + | 
|  | 230 | +### Integration Testing | 
|  | 231 | + | 
|  | 232 | +Create acceptance tests that verify the locators work with real DOM: | 
|  | 233 | + | 
|  | 234 | +```js | 
|  | 235 | +Scenario('should use custom locators', I => { | 
|  | 236 | +  I.amOnPage('/test-page') | 
|  | 237 | +  I.seeElement({ byRole: 'navigation' }) | 
|  | 238 | +  I.click({ byTestId: 'submit-button' }) | 
|  | 239 | +  I.see('Success', { byAriaLabel: 'status-message' }) | 
|  | 240 | +}) | 
|  | 241 | +``` | 
|  | 242 | + | 
|  | 243 | +## Migration from Other Helpers | 
|  | 244 | + | 
|  | 245 | +If you're migrating from WebDriver helper that already supports custom locators, the syntax is identical: | 
|  | 246 | + | 
|  | 247 | +```js | 
|  | 248 | +// WebDriver and Playwright both support this syntax: | 
|  | 249 | +I.click({ byTestId: 'submit' }) | 
|  | 250 | +I.fillField({ byRole: 'textbox' }, 'value') | 
|  | 251 | +``` | 
|  | 252 | + | 
|  | 253 | +## Troubleshooting | 
|  | 254 | + | 
|  | 255 | +### Common Issues | 
|  | 256 | + | 
|  | 257 | +1. **Locator not recognized**: Ensure the strategy is defined in `customLocatorStrategies` and is a function. | 
|  | 258 | + | 
|  | 259 | +2. **Elements not found**: Check that your locator function returns the correct element or null. | 
|  | 260 | + | 
|  | 261 | +3. **Multiple elements**: If your function returns an array, interactions will use the first element. | 
|  | 262 | + | 
|  | 263 | +4. **Timing issues**: Custom locators work with all waiting methods (`waitForElement`, etc.). | 
|  | 264 | + | 
|  | 265 | +### Debug Mode | 
|  | 266 | + | 
|  | 267 | +Enable debug mode to see locator resolution: | 
|  | 268 | + | 
|  | 269 | +```js | 
|  | 270 | +// In codecept.conf.js | 
|  | 271 | +exports.config = { | 
|  | 272 | +  helpers: { | 
|  | 273 | +    Playwright: { | 
|  | 274 | +      // ... other config | 
|  | 275 | +    }, | 
|  | 276 | +  }, | 
|  | 277 | +  plugins: { | 
|  | 278 | +    stepByStepReport: { | 
|  | 279 | +      enabled: true, | 
|  | 280 | +    }, | 
|  | 281 | +  }, | 
|  | 282 | +} | 
|  | 283 | +``` | 
|  | 284 | + | 
|  | 285 | +### Verbose Logging | 
|  | 286 | + | 
|  | 287 | +Custom locator registration is logged when the helper starts: | 
|  | 288 | + | 
|  | 289 | +``` | 
|  | 290 | +Playwright: registering custom locator strategy: byRole | 
|  | 291 | +Playwright: registering custom locator strategy: byTestId | 
|  | 292 | +``` | 
0 commit comments