Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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' }, '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+
```

‎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]'},'miles@davis.com');
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'}, 'test@example.com');
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)

AltStyle によって変換されたページ (->オリジナル) /