Skip to content

Commit 2e814fd

Browse files
authored
Add Custom Strategy Locators support to Playwright helper (#5090)
1 parent 663be8d commit 2e814fd

File tree

10 files changed

+1531
-211
lines changed

10 files changed

+1531
-211
lines changed

docs/custom-locators-playwright.md

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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' }, '[email protected]')
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+
```

docs/playwright.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,52 @@ I.fillField({name: 'user[email]'},'[email protected]');
130130
I.seeElement({xpath: '//body/header'});
131131
```
132132

133+
### Custom Locator Strategies
134+
135+
CodeceptJS with Playwright supports custom locator strategies, allowing you to define your own element finding logic. Custom locator strategies are JavaScript functions that receive a selector value and return DOM elements.
136+
137+
To use custom locator strategies, configure them in your `codecept.conf.js`:
138+
139+
```js
140+
exports.config = {
141+
helpers: {
142+
Playwright: {
143+
url: 'http://localhost',
144+
browser: 'chromium',
145+
customLocatorStrategies: {
146+
byRole: (selector, root) => {
147+
return root.querySelector(`[role="${selector}"]`);
148+
},
149+
byTestId: (selector, root) => {
150+
return root.querySelector(`[data-testid="${selector}"]`);
151+
},
152+
byDataQa: (selector, root) => {
153+
const elements = root.querySelectorAll(`[data-qa="${selector}"]`);
154+
return Array.from(elements); // Return array for multiple elements
155+
}
156+
}
157+
}
158+
}
159+
}
160+
```
161+
162+
Once configured, you can use these custom locator strategies in your tests:
163+
164+
```js
165+
I.click({byRole: 'button'}); // Find by role attribute
166+
I.see('Welcome', {byTestId: 'title'}); // Find by data-testid
167+
I.fillField({byDataQa: 'email'}, '[email protected]');
168+
```
169+
170+
**Custom Locator Function Guidelines:**
171+
- Functions receive `(selector, root)` parameters where `selector` is the value and `root` is the DOM context
172+
- Return a single DOM element for finding the first match
173+
- Return an array of DOM elements for finding all matches
174+
- Return `null` or empty array if no elements found
175+
- Functions execute in the browser context, so only browser APIs are available
176+
177+
This feature provides the same functionality as WebDriver's custom locator strategies but leverages Playwright's native selector engine system.
178+
133179
### Interactive Pause
134180

135181
It's easy to start writing a test if you use [interactive pause](/basics#debug). Just open a web page and pause execution.

0 commit comments

Comments
 (0)