Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 292 additions & 0 deletions docs/custom-locators-playwright.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
# 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:

```js
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

```js
// 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

```js
// Use with within() blocks
within({ byRole: 'form' }, () => {
I.fillField({ byTestId: 'email' }, 'test@example.com')
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

```js
(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

```js
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

```js
// 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:

```js
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:

```js
// 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:

```js
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:

```js
byClass: (selector, root) => {
const elements = root.querySelectorAll(`.${selector}`)
return Array.from(elements) // Convert NodeList to Array
}
```

### 4. Performance

Keep locator functions simple and fast:

```js
// 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:

```js
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:

```js
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:

```js
// WebDriver and Playwright both support this syntax:
I.click({ byTestId: 'submit' })
I.fillField({ byRole: 'textbox' }, 'value')
```

## Troubleshooting

### Common Issues

1. **Locator not recognized**: Ensure the strategy is defined in `customLocatorStrategies` and is a function.

2. **Elements not found**: Check that your locator function returns the correct element or null.

3. **Multiple elements**: If your function returns an array, interactions will use the first element.

4. **Timing issues**: Custom locators work with all waiting methods (`waitForElement`, etc.).

### Debug Mode

Enable debug mode to see locator resolution:

```js
// 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
```
46 changes: 46 additions & 0 deletions docs/playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,52 @@ I.fillField({name: 'user[email]'},'miles@davis.com');
I.seeElement({xpath: '//body/header'});
```

### Custom Locator Strategies

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.

To use custom locator strategies, configure them in your `codecept.conf.js`:

```js
exports.config = {
helpers: {
Playwright: {
url: 'http://localhost',
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
}
}
}
}
}
```

Once configured, you can use these custom locator strategies in your tests:

```js
I.click({byRole: 'button'}); // Find by role attribute
I.see('Welcome', {byTestId: 'title'}); // Find by data-testid
I.fillField({byDataQa: 'email'}, 'test@example.com');
```

**Custom Locator Function Guidelines:**
- Functions receive `(selector, root)` parameters where `selector` is the value and `root` is the DOM context
- Return a single DOM element for finding the first match
- Return an array of DOM elements for finding all matches
- Return `null` or empty array if no elements found
- Functions execute in the browser context, so only browser APIs are available

This feature provides the same functionality as WebDriver's custom locator strategies but leverages Playwright's native selector engine system.

### Interactive Pause

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