diff --git a/docs/custom-locators-playwright.md b/docs/custom-locators-playwright.md deleted file mode 100644 index 58a04a62d..000000000 --- a/docs/custom-locators-playwright.md +++ /dev/null @@ -1,292 +0,0 @@ -# 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 -``` diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md index cf23c8afd..cb32d3504 100644 --- a/docs/helpers/Playwright.md +++ b/docs/helpers/Playwright.md @@ -80,7 +80,6 @@ Type: [object][6] * `highlightElement` **[boolean][28]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). * `recordHar` **[object][6]?** record HAR and will be saved to `output/har`. See more of [HAR options][3]. * `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][51]. -* `customLocatorStrategies` **[object][6]?** custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(`[role="${selector}"]`) } }` * `storageState` **([string][9] | [object][6])?** Playwright storage state (path to JSON file or object) passed directly to `browser.newContext`. If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`), @@ -88,17 +87,6 @@ Type: [object][6] May include session cookies, auth tokens, localStorage and (if captured with `grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit. -## createCustomSelectorEngine - -Creates a Playwright selector engine factory for a custom locator strategy. - -### Parameters - -* `name` **[string][9]** Strategy name for error messages -* `func` **[Function][22]** The locator function (selector, root) => Element|Element[] - -Returns **[Function][22]** Selector engine factory - ## handleRoleLocator Handles role locator objects by converting them to Playwright's getByRole() API diff --git a/docs/playwright.md b/docs/playwright.md index d3a9d8121..36d0c08a7 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -131,52 +131,6 @@ 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. diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index eb109eb08..43cb5c337 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -36,44 +36,12 @@ import WebElement from '../element/WebElement.js' let playwright let perfTiming let defaultSelectorEnginesInitialized = false -let registeredCustomLocatorStrategies = new Set() -let globalCustomLocatorStrategies = new Map() // Use global object to track selector registration across workers if (typeof global.__playwrightSelectorsRegistered === 'undefined') { global.__playwrightSelectorsRegistered = false } -/** - * Creates a Playwright selector engine factory for a custom locator strategy. - * @param {string} name - Strategy name for error messages - * @param {Function} func - The locator function (selector, root) => Element|Element[] - * @returns {Function} Selector engine factory - */ -function createCustomSelectorEngine(name, func) { - return () => ({ - create: () => null, - query(root, selector) { - if (!root) return null - try { - const result = func(selector, root) - return Array.isArray(result) ? result[0] : result - } catch (e) { - return null - } - }, - queryAll(root, selector) { - if (!root) return [] - try { - const result = func(selector, root) - return Array.isArray(result) ? result : result ? [result] : [] - } catch (e) { - return [] - } - }, - }) -} - const popupStore = new Popup() const consoleLogStore = new Console() const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron'] @@ -130,7 +98,6 @@ const pathSeparator = path.sep * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). * @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har). * @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id). - * @prop {object} [customLocatorStrategies] - custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(`[role="${selector}"]`) } }` * @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object) * passed directly to `browser.newContext`. * If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`), @@ -386,18 +353,6 @@ class Playwright extends Helper { this.recordedWebSocketMessagesAtLeastOnce = false this.cdpSession = null - // Filter out invalid customLocatorStrategies (empty arrays, objects without functions) - // This can happen in worker threads where config is serialized/deserialized - this.customLocatorStrategies = this._parseCustomLocatorStrategies(config.customLocatorStrategies) - this._customLocatorsRegistered = false - - // Add custom locator strategies to global registry for early registration - if (this.customLocatorStrategies) { - for (const [name, func] of Object.entries(this.customLocatorStrategies)) { - globalCustomLocatorStrategies.set(name, func) - } - } - // Add test failure tracking to prevent false positives this.testFailures = [] this.hasCleanupError = false @@ -543,16 +498,6 @@ class Playwright extends Helper { } } - // Ensure custom locators from this instance are in the global registry - // This is critical for worker threads where globalCustomLocatorStrategies is a new Map - if (this.customLocatorStrategies) { - for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) { - if (!globalCustomLocatorStrategies.has(strategyName)) { - globalCustomLocatorStrategies.set(strategyName, strategyFunction) - } - } - } - // register an internal selector engine for reading value property of elements in a selector try { // Always wrap in try-catch since selectors might be registered globally across workers @@ -583,28 +528,11 @@ class Playwright extends Helper { // Ignore if already set } } - - // Register all custom locator strategies from the global registry - await this._registerGlobalCustomLocators() } catch (e) { console.warn(e) } } - async _registerGlobalCustomLocators() { - for (const [name, func] of globalCustomLocatorStrategies.entries()) { - if (registeredCustomLocatorStrategies.has(name)) continue - try { - await playwright.selectors.register(name, createCustomSelectorEngine(name, func)) - registeredCustomLocatorStrategies.add(name) - } catch (e) { - if (!e.message.includes('already registered')) { - this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`) - } - } - } - } - _beforeSuite() { // Skip browser start in dry-run mode (used by check command) if (store.dryRun) { @@ -1266,33 +1194,6 @@ class Playwright extends Helper { return this.browser } - _hasCustomLocatorStrategies() { - return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0) - } - - _parseCustomLocatorStrategies(strategies) { - if (typeof strategies !== 'object' || strategies === null) return null - const hasValidFunctions = Object.values(strategies).some(v => typeof v === 'function') - return hasValidFunctions ? strategies : null - } - - _lookupCustomLocator(customStrategy) { - if (!this._hasCustomLocatorStrategies()) return null - const strategy = this.customLocatorStrategies[customStrategy] - return typeof strategy === 'function' ? strategy : null - } - - _isCustomLocator(locator) { - const locatorObj = new Locator(locator) - if (!locatorObj.isCustom()) return false - if (this._lookupCustomLocator(locatorObj.type)) return true - throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".') - } - - _isCustomLocatorStrategyDefined() { - return this._hasCustomLocatorStrategies() - } - /** * Create a new browser context with a page. \ * Usually it should be run from a custom helper after call of `_startBrowser()` @@ -1304,30 +1205,11 @@ class Playwright extends Helper { } this.browserContext = await this.browser.newContext(contextOptions) - // Register custom locator strategies for this context - await this._registerCustomLocatorStrategies() - const page = await this.browserContext.newPage() targetCreatedHandler.call(this, page) await this._setPage(page) } - async _registerCustomLocatorStrategies() { - if (!this._hasCustomLocatorStrategies()) return - - for (const [name, func] of Object.entries(this.customLocatorStrategies)) { - if (registeredCustomLocatorStrategies.has(name)) continue - try { - await playwright.selectors.register(name, createCustomSelectorEngine(name, func)) - registeredCustomLocatorStrategies.add(name) - } catch (e) { - if (!e.message.includes('already registered')) { - this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`) - } - } - } - } - _getType() { return this.browser._type } @@ -2722,23 +2604,12 @@ class Playwright extends Helper { _contextLocator(locator) { const locatorObj = new Locator(locator, 'css') - // Handle custom locators differently - if (locatorObj.isCustom()) { - return buildCustomLocatorString(locatorObj) - } - locator = buildLocatorString(locatorObj) if (this.contextLocator) { const contextLocatorObj = new Locator(this.contextLocator, 'css') - if (contextLocatorObj.isCustom()) { - // For custom context locators, we can't use the >> syntax - // Instead, we'll need to handle this differently in the calling methods - return locator - } else { - const contextLocator = buildLocatorString(contextLocatorObj) - locator = `${contextLocator} >> ${locator}` - } + const contextLocator = buildLocatorString(contextLocatorObj) + locator = `${contextLocator} >> ${locator}` } return locator @@ -2762,30 +2633,18 @@ class Playwright extends Helper { const locatorObj = new Locator(locator, 'css') - if (locatorObj.isCustom()) { - // For custom locators, find the element first - const elements = await findCustomElements.call(this, this.page, locatorObj) - if (elements.length === 0) { - throw new Error(`Element not found: ${locatorObj.toString()}`) - } - const text = await elements[0].textContent() - assertElementExists(text, locatorObj.toString()) + locator = this._contextLocator(locator) + try { + const text = await this.page.textContent(locator) + assertElementExists(text, locator) this.debugSection('Text', text) return text - } else { - locator = this._contextLocator(locator) - try { - const text = await this.page.textContent(locator) - assertElementExists(text, locator) - this.debugSection('Text', text) - return text - } catch (error) { - // Convert Playwright timeout errors to ElementNotFound for consistency - if (error.message && error.message.includes('Timeout')) { - throw new ElementNotFound(locator, 'text') - } - throw error + } catch (error) { + // Convert Playwright timeout errors to ElementNotFound for consistency + if (error.message && error.message.includes('Timeout')) { + throw new ElementNotFound(locator, 'text') } + throw error } } @@ -3350,16 +3209,7 @@ class Playwright extends Helper { const context = await this._getContext() try { - if (locator.isCustom()) { - // For custom locators, we need to use our custom element finding logic - const elements = await findCustomElements.call(this, context, locator) - if (elements.length === 0) { - throw new Error(`Custom locator ${locator.type}=${locator.value} not found`) - } - await elements[0].waitFor({ timeout: waitTimeout, state: 'attached' }) - } else { - await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' }) - } + await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' }) } catch (e) { throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`) } @@ -3377,26 +3227,6 @@ class Playwright extends Helper { const context = await this._getContext() let count = 0 - // Handle custom locators - if (locator.isCustom()) { - let waiter - do { - const elements = await findCustomElements.call(this, context, locator) - if (elements.length > 0) { - waiter = await elements[0].isVisible() - } else { - waiter = false - } - if (!waiter) { - await this.wait(1) - count += 1000 - } - } while (!waiter && count <= waitTimeout) - - if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`) - return - } - // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented let waiter if (this.frame) { @@ -3577,15 +3407,6 @@ class Playwright extends Helper { if (context) { const locator = new Locator(context, 'css') try { - if (locator.isCustom()) { - // For custom locators, find the elements first then check for text within them - const elements = await findCustomElements.call(this, contextObject, locator) - if (elements.length === 0) { - throw new Error(`Context element not found: ${locator.toString()}`) - } - return elements[0].locator(`text=${text}`).first().waitFor({ timeout: waitTimeout, state: 'visible' }) - } - if (!locator.isXPath()) { return contextObject .locator(`${locator.simplify()} >> text=${text}`) @@ -3828,7 +3649,7 @@ class Playwright extends Helper { if (!locator.isXPath()) { try { await context - .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`) + .locator(locator.simplify()) .first() .waitFor({ timeout: waitTimeout, state: 'detached' }) } catch (e) { @@ -4253,16 +4074,7 @@ class Playwright extends Helper { export default Playwright -function buildCustomLocatorString(locator) { - // Note: this.debug not available in standalone function, using console.log - console.log(`Building custom locator string: ${locator.type}=${locator.value}`) - return `${locator.type}=${locator.value}` -} - function buildLocatorString(locator) { - if (locator.isCustom()) { - return buildCustomLocatorString(locator) - } if (locator.isXPath()) { return `xpath=${locator.value}` } @@ -4306,119 +4118,11 @@ async function findElements(matcher, locator) { locator = new Locator(locator, 'css') - // Handle custom locators directly instead of relying on Playwright selector engines - if (locator.isCustom()) { - return findCustomElements.call(this, matcher, locator) - } - - // Check if we have a custom context locator and need to search within it - if (this.contextLocator) { - const contextLocatorObj = new Locator(this.contextLocator, 'css') - if (contextLocatorObj.isCustom()) { - // Find the context elements first - const contextElements = await findCustomElements.call(this, matcher, contextLocatorObj) - if (contextElements.length === 0) { - return [] - } - - // Search within the first context element - const locatorString = buildLocatorString(locator) - return contextElements[0].locator(locatorString).all() - } - } - const locatorString = buildLocatorString(locator) return matcher.locator(locatorString).all() } -async function findCustomElements(matcher, locator) { - // Always prioritize this.customLocatorStrategies which is set in constructor from config - // and persists in every worker thread instance - let strategyFunction = null - - if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) { - strategyFunction = this.customLocatorStrategies[locator.type] - } else if (globalCustomLocatorStrategies.has(locator.type)) { - // Fallback to global registry (populated in constructor and _init) - strategyFunction = globalCustomLocatorStrategies.get(locator.type) - } - - if (!strategyFunction) { - throw new Error(`Custom locator strategy "${locator.type}" is not defined. Please define "customLocatorStrategies" in your configuration.`) - } - - // Execute the custom locator function in the browser context using page.evaluate - const page = matcher.constructor.name === 'Page' ? matcher : await matcher.page() - - const elements = await page.evaluate( - ({ strategyCode, selector }) => { - const strategy = new Function('return ' + strategyCode)() - const result = strategy(selector, document) - - // Convert NodeList or single element to array - if (result && result.nodeType) { - return [result] - } else if (result && result.length !== undefined) { - return Array.from(result) - } else if (Array.isArray(result)) { - return result - } - - return [] - }, - { - strategyCode: strategyFunction.toString(), - selector: locator.value, - }, - ) - - // Convert the found elements back to Playwright locators - if (elements.length === 0) { - return [] - } - - // Create CSS selectors for the found elements and return as locators - const locators = [] - const timestamp = Date.now() - - for (let i = 0; i < elements.length; i++) { - // Use a unique attribute approach to target specific elements - const uniqueAttr = `data-codecept-custom-${timestamp}-${i}` - - await page.evaluate( - ({ index, uniqueAttr, strategyCode, selector }) => { - // Re-execute the strategy to find elements and mark the specific one - const strategy = new Function('return ' + strategyCode)() - const result = strategy(selector, document) - - let elementsArray = [] - if (result && result.nodeType) { - elementsArray = [result] - } else if (result && result.length !== undefined) { - elementsArray = Array.from(result) - } else if (Array.isArray(result)) { - elementsArray = result - } - - if (elementsArray[index]) { - elementsArray[index].setAttribute(uniqueAttr, 'true') - } - }, - { - index: i, - uniqueAttr, - strategyCode: strategyFunction.toString(), - selector: locator.value, - }, - ) - - locators.push(page.locator(`[${uniqueAttr}="true"]`)) - } - - return locators -} - async function findElement(matcher, locator) { if (locator.react) return findReact(matcher, locator) if (locator.vue) return findVue(matcher, locator) diff --git a/lib/helper/extras/PlaywrightLocator.js b/lib/helper/extras/PlaywrightLocator.js deleted file mode 100644 index c7122939c..000000000 --- a/lib/helper/extras/PlaywrightLocator.js +++ /dev/null @@ -1,110 +0,0 @@ -import Locator from '../../locator.js' - -function buildLocatorString(locator) { - if (locator.isCustom()) { - return `${locator.type}=${locator.value}` - } - if (locator.isXPath()) { - return `xpath=${locator.value}` - } - return locator.simplify() -} - -async function findElements(matcher, locator) { - const matchedLocator = new Locator(locator, 'css') - - if (matchedLocator.type === 'react') return findReact(matcher, matchedLocator) - if (matchedLocator.type === 'vue') return findVue(matcher, matchedLocator) - if (matchedLocator.type === 'pw') return findByPlaywrightLocator(matcher, matchedLocator) - if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator) - - return matcher.locator(buildLocatorString(matchedLocator)).all() -} - -async function findElement(matcher, locator) { - const matchedLocator = new Locator(locator, 'css') - - if (matchedLocator.type === 'react') return findReact(matcher, matchedLocator) - if (matchedLocator.type === 'vue') return findVue(matcher, matchedLocator) - if (matchedLocator.type === 'pw') return findByPlaywrightLocator(matcher, matchedLocator, { first: true }) - if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator, { first: true }) - - return matcher.locator(buildLocatorString(matchedLocator)).first() -} - -async function getVisibleElements(elements) { - const visibleElements = [] - for (const element of elements) { - if (await element.isVisible()) { - visibleElements.push(element) - } - } - if (visibleElements.length === 0) { - return elements - } - return visibleElements -} - -async function findReact(matcher, locator) { - const details = locator.locator ?? { react: locator.value } - let locatorString = `_react=${details.react}` - - if (details.props) { - locatorString += propBuilder(details.props) - } - - return matcher.locator(locatorString).all() -} - -async function findVue(matcher, locator) { - const details = locator.locator ?? { vue: locator.value } - let locatorString = `_vue=${details.vue}` - - if (details.props) { - locatorString += propBuilder(details.props) - } - - return matcher.locator(locatorString).all() -} - -async function findByPlaywrightLocator(matcher, locator, { first = false } = {}) { - const details = locator.locator ?? { pw: locator.value } - const locatorValue = details.pw - - const handle = matcher.locator(locatorValue) - return first ? handle.first() : handle.all() -} - -async function findByRole(matcher, locator, { first = false } = {}) { - const details = locator.locator ?? { role: locator.value } - const { role, text, name, exact, includeHidden, ...rest } = details - const options = { ...rest } - - if (includeHidden !== undefined) options.includeHidden = includeHidden - - const accessibleName = name ?? text - if (accessibleName !== undefined) { - options.name = accessibleName - if (exact === true) options.exact = true - } - - const roleLocator = matcher.getByRole(role, options) - return first ? roleLocator.first() : roleLocator.all() -} - -function propBuilder(props) { - let _props = '' - - for (const [key, value] of Object.entries(props)) { - if (typeof value === 'object') { - for (const [k, v] of Object.entries(value)) { - _props += `[${key}.${k} = "${v}"]` - } - } else { - _props += `[${key} = "${value}"]` - } - } - return _props -} - -export { buildLocatorString, findElements, findElement, getVisibleElements, findReact, findVue, findByPlaywrightLocator, findByRole } diff --git a/lib/workers.js b/lib/workers.js index 4e77fc6a8..0ed3a71b3 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -224,7 +224,8 @@ class WorkerObject { const oldConfig = JSON.parse(this.options.override || '{}') // Remove customLocatorStrategies from both old and new config before JSON serialization - // since functions cannot be serialized and will be lost, causing workers to have empty strategies + // since functions cannot be serialized and will be lost, causing workers to have empty strategies. + // Note: Only WebDriver helper supports customLocatorStrategies const configWithoutFunctions = { ...config } // Clean both old and new config diff --git a/test/acceptance/codecept.Playwright.CustomLocators.js b/test/acceptance/codecept.Playwright.CustomLocators.js deleted file mode 100644 index fbc162e61..000000000 --- a/test/acceptance/codecept.Playwright.CustomLocators.js +++ /dev/null @@ -1,34 +0,0 @@ -import { config } from '../acceptance/codecept.Playwright.js' - -// Extend the base Playwright configuration to add custom locator strategies -const customLocatorConfig = { - ...config, - grep: null, // Remove grep filter to run custom locator tests - helpers: { - ...config.helpers, - Playwright: { - ...config.helpers.Playwright, - browser: process.env.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) - }, - byAriaLabel: (selector, root) => { - return root.querySelector(`[aria-label="${selector}"]`) - }, - byPlaceholder: (selector, root) => { - return root.querySelector(`[placeholder="${selector}"]`) - }, - }, - }, - }, -} - -export { customLocatorConfig as config } diff --git a/test/acceptance/codecept.Playwright.js b/test/acceptance/codecept.Playwright.js index e646e162f..b80142ec2 100644 --- a/test/acceptance/codecept.Playwright.js +++ b/test/acceptance/codecept.Playwright.js @@ -20,23 +20,6 @@ export const config = { webkit: { ignoreHTTPSErrors: true, }, - customLocatorStrategies: { - byRole: (selector, root) => { - return root.querySelector(`[role="${selector}"]`) - }, - byTestId: (selector, root) => { - return root.querySelector(`[data-testid="${selector}"]`) - }, - byDataQa: (selector, root) => { - return root.querySelectorAll(`[data-qa="${selector}"]`) - }, - byAriaLabel: (selector, root) => { - return root.querySelector(`[aria-label="${selector}"]`) - }, - byPlaceholder: (selector, root) => { - return root.querySelector(`[placeholder="${selector}"]`) - }, - }, }, JSONResponse: { requestHelper: 'Playwright', diff --git a/test/acceptance/custom_locators_test.js b/test/acceptance/custom_locators_test.js deleted file mode 100644 index 0ff69d03f..000000000 --- a/test/acceptance/custom_locators_test.js +++ /dev/null @@ -1,125 +0,0 @@ -const { I } = inject() - -Feature('Custom Locator Strategies - @Playwright') - -Before(() => { - I.amOnPage('/form/custom_locator_strategies') -}) - -Scenario('should find elements using byRole custom locator', ({ I }) => { - I.see('Custom Locator Test Page', { byRole: 'main' }) - I.seeElement({ byRole: 'form' }) - I.seeElement({ byRole: 'button' }) - I.seeElement({ byRole: 'navigation' }) - I.seeElement({ byRole: 'complementary' }) -}) - -Scenario('should find elements using byTestId custom locator', ({ I }) => { - I.see('Custom Locator Test Page', { byTestId: 'page-title' }) - I.seeElement({ byTestId: 'username-input' }) - I.seeElement({ byTestId: 'password-input' }) - I.seeElement({ byTestId: 'submit-button' }) - I.seeElement({ byTestId: 'cancel-button' }) - I.seeElement({ byTestId: 'info-text' }) -}) - -Scenario('should find elements using byDataQa custom locator', ({ I }) => { - I.seeElement({ byDataQa: 'test-form' }) - I.seeElement({ byDataQa: 'form-section' }) - I.seeElement({ byDataQa: 'submit-btn' }) - I.seeElement({ byDataQa: 'cancel-btn' }) - I.seeElement({ byDataQa: 'info-section' }) - I.seeElement({ byDataQa: 'nav-section' }) -}) - -Scenario('should find elements using byAriaLabel custom locator', ({ I }) => { - I.see('Custom Locator Test Page', { byAriaLabel: 'Welcome Message' }) - I.seeElement({ byAriaLabel: 'Username field' }) - I.seeElement({ byAriaLabel: 'Password field' }) - I.seeElement({ byAriaLabel: 'Submit form' }) - I.seeElement({ byAriaLabel: 'Cancel form' }) - I.seeElement({ byAriaLabel: 'Information message' }) -}) - -Scenario('should find elements using byPlaceholder custom locator', ({ I }) => { - I.seeElement({ byPlaceholder: 'Enter your username' }) - I.seeElement({ byPlaceholder: 'Enter your password' }) -}) - -Scenario('should interact with elements using custom locators', ({ I }) => { - I.fillField({ byTestId: 'username-input' }, 'testuser') - I.fillField({ byPlaceholder: 'Enter your password' }, 'password123') - - I.seeInField({ byTestId: 'username-input' }, 'testuser') - I.seeInField({ byAriaLabel: 'Password field' }, 'password123') - - I.click({ byDataQa: 'submit-btn' }) - // Form submission would normally happen here -}) - -Scenario('should handle multiple elements with byDataQa locator', ({ I }) => { - // byDataQa returns all matching elements, but interactions use the first one - I.seeElement({ byDataQa: 'form-section' }) - - // Should be able to see both form sections exist - I.executeScript(() => { - const sections = document.querySelectorAll('[data-qa="form-section"]') - if (sections.length !== 2) { - throw new Error(`Expected 2 form sections, found ${sections.length}`) - } - }) -}) - -Scenario('should work with complex selectors and mixed locator types', ({ I }) => { - // Test that custom locators work alongside standard ones - within({ byRole: 'form' }, () => { - I.seeElement({ byTestId: 'username-input' }) - I.seeElement('input[name="password"]') // Standard CSS selector - I.seeElement({ xpath: '//button[@type="submit"]' }) // Standard XPath - }) - - within({ byDataQa: 'nav-section' }, () => { - I.seeElement({ byAriaLabel: 'Home link' }) - I.seeElement({ byAriaLabel: 'About link' }) - I.seeElement({ byAriaLabel: 'Contact link' }) - }) -}) - -Scenario.skip('should fail gracefully for non-existent custom locators', async ({ I }) => { - // This should throw an error about undefined custom locator strategy - let errorThrown = false - let errorMessage = '' - - try { - await I.seeElement({ byCustomUndefined: 'test' }) - } catch (error) { - errorThrown = true - errorMessage = error.message - } - - if (!errorThrown) { - throw new Error('Should have thrown an error for undefined custom locator') - } - - if (!errorMessage.includes('Please define "customLocatorStrategies"')) { - throw new Error('Wrong error message: ' + errorMessage) - } -}) - -Scenario('should work with grabbing methods', async ({ I }) => { - const titleText = await I.grabTextFrom({ byTestId: 'page-title' }) - I.expectEqual(titleText, 'Custom Locator Test Page') - - const usernameValue = await I.grabValueFrom({ byAriaLabel: 'Username field' }) - I.expectEqual(usernameValue, '') - - I.fillField({ byPlaceholder: 'Enter your username' }, 'grabtest') - const newUsernameValue = await I.grabValueFrom({ byTestId: 'username-input' }) - I.expectEqual(newUsernameValue, 'grabtest') -}) - -Scenario('should work with waiting methods', ({ I }) => { - I.waitForElement({ byRole: 'main' }, 2) - I.waitForVisible({ byTestId: 'submit-button' }, 2) - I.waitForText('Custom Locator Test Page', 2, { byAriaLabel: 'Welcome Message' }) -}) diff --git a/test/helper/CustomLocator_test.js b/test/helper/CustomLocator_test.js deleted file mode 100644 index 3cae03ef9..000000000 --- a/test/helper/CustomLocator_test.js +++ /dev/null @@ -1,247 +0,0 @@ -import * as chai from 'chai' -import Playwright from '../../lib/helper/Playwright.js' -import Locator from '../../lib/locator.js' - -const expect = chai.expect - -describe('Custom Locator Strategies', function () { - this.timeout(5000) - - let helper - - beforeEach(() => { - helper = new 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) - }, - }, - }) - }) - - describe('#configuration', () => { - it('should store custom locator strategies in helper', () => { - expect(helper.customLocatorStrategies).to.not.be.undefined - expect(helper.customLocatorStrategies).to.be.an('object') - expect(Object.keys(helper.customLocatorStrategies)).to.have.length(3) - }) - - it('should have all configured strategies as functions', () => { - expect(helper.customLocatorStrategies.byRole).to.be.a('function') - expect(helper.customLocatorStrategies.byTestId).to.be.a('function') - expect(helper.customLocatorStrategies.byDataQa).to.be.a('function') - }) - }) - - describe('#_isCustomLocatorStrategyDefined', () => { - it('should return true when strategies are defined', () => { - expect(helper._isCustomLocatorStrategyDefined()).to.be.true - }) - - it('should return false when no strategies are defined', () => { - const emptyHelper = new Playwright({ - url: 'http://localhost', - browser: 'chromium', - }) - expect(emptyHelper._isCustomLocatorStrategyDefined()).to.be.false - }) - - it('should return false when strategies is empty object', () => { - const emptyHelper = new Playwright({ - url: 'http://localhost', - browser: 'chromium', - customLocatorStrategies: {}, - }) - expect(emptyHelper._isCustomLocatorStrategyDefined()).to.be.false - }) - }) - - describe('#_lookupCustomLocator', () => { - it('should find existing custom locator function', () => { - const strategy = helper._lookupCustomLocator('byRole') - expect(strategy).to.be.a('function') - }) - - it('should return null for non-existent strategy', () => { - const strategy = helper._lookupCustomLocator('nonExistent') - expect(strategy).to.be.null - }) - - it('should return null when customLocatorStrategies is not defined', () => { - const emptyHelper = new Playwright({ - url: 'http://localhost', - browser: 'chromium', - }) - const strategy = emptyHelper._lookupCustomLocator('byRole') - expect(strategy).to.be.null - }) - - it('should return null when customLocatorStrategies is null', () => { - const helper = new Playwright({ - url: 'http://localhost', - browser: 'chromium', - }) - helper.customLocatorStrategies = null - const strategy = helper._lookupCustomLocator('byRole') - expect(strategy).to.be.null - }) - - it('should return null when strategy exists but is not a function', () => { - const helper = new Playwright({ - url: 'http://localhost', - browser: 'chromium', - customLocatorStrategies: { - notAFunction: 'this is a string, not a function', - }, - }) - const strategy = helper._lookupCustomLocator('notAFunction') - expect(strategy).to.be.null - }) - }) - - describe('#_isCustomLocator', () => { - it('should identify custom locators correctly', () => { - expect(helper._isCustomLocator({ byRole: 'button' })).to.be.true - expect(helper._isCustomLocator({ byTestId: 'submit' })).to.be.true - expect(helper._isCustomLocator({ byDataQa: 'form' })).to.be.true - }) - - it('should not identify standard locators as custom', () => { - expect(helper._isCustomLocator({ css: '#test' })).to.be.false - expect(helper._isCustomLocator({ xpath: '//div' })).to.be.false - expect(helper._isCustomLocator({ id: 'test' })).to.be.false - expect(helper._isCustomLocator({ name: 'test' })).to.be.false - }) - - it('should throw error for undefined custom strategy', () => { - expect(() => { - helper._isCustomLocator({ undefinedStrategy: 'test' }) - }).to.throw('Please define "customLocatorStrategies"') - }) - - it('should handle string locators correctly', () => { - expect(helper._isCustomLocator('#test')).to.be.false - expect(helper._isCustomLocator('//div')).to.be.false - }) - - it('should handle edge case locators', () => { - expect(helper._isCustomLocator(null)).to.be.false - expect(helper._isCustomLocator(undefined)).to.be.false - expect(helper._isCustomLocator({})).to.be.false - }) - }) - - describe('Locator class integration', () => { - it('should identify custom locator types via Locator class', () => { - const customLocator = new Locator({ byRole: 'button' }) - expect(customLocator.isCustom()).to.be.true - expect(customLocator.type).to.equal('byRole') - expect(customLocator.value).to.equal('button') - }) - - it('should not identify standard locator types as custom', () => { - const standardLocator = new Locator({ css: '#test' }) - expect(standardLocator.isCustom()).to.be.false - }) - - it('should handle multiple custom locator strategies in one helper', () => { - const multiHelper = new Playwright({ - url: 'http://localhost', - browser: 'chromium', - customLocatorStrategies: { - byRole: (s, r) => r.querySelector(`[role="${s}"]`), - byTestId: (s, r) => r.querySelector(`[data-testid="${s}"]`), - byClass: (s, r) => r.querySelector(`.${s}`), - byAttribute: (s, r) => r.querySelector(`[${s}]`), - byCustom: (s, r) => r.querySelector(s), - }, - }) - - expect(multiHelper._isCustomLocatorStrategyDefined()).to.be.true - expect(Object.keys(multiHelper.customLocatorStrategies)).to.have.length(5) - - // Test each strategy - expect(multiHelper._isCustomLocator({ byRole: 'button' })).to.be.true - expect(multiHelper._isCustomLocator({ byTestId: 'test' })).to.be.true - expect(multiHelper._isCustomLocator({ byClass: 'active' })).to.be.true - expect(multiHelper._isCustomLocator({ byAttribute: 'disabled' })).to.be.true - expect(multiHelper._isCustomLocator({ byCustom: '#custom' })).to.be.true - }) - }) - - describe('Error handling', () => { - it('should handle malformed configuration gracefully', () => { - // Test with non-object customLocatorStrategies - const badHelper1 = new Playwright({ - url: 'http://localhost', - browser: 'chromium', - customLocatorStrategies: 'not an object', - }) - expect(badHelper1._isCustomLocatorStrategyDefined()).to.be.false - expect(badHelper1._lookupCustomLocator('anything')).to.be.null - - // Test with null customLocatorStrategies - const badHelper2 = new Playwright({ - url: 'http://localhost', - browser: 'chromium', - customLocatorStrategies: null, - }) - expect(badHelper2._isCustomLocatorStrategyDefined()).to.be.false - expect(badHelper2._lookupCustomLocator('anything')).to.be.null - }) - - it('should validate strategy functions correctly', () => { - const mixedHelper = new Playwright({ - url: 'http://localhost', - browser: 'chromium', - customLocatorStrategies: { - validStrategy: (s, r) => r.querySelector(s), - invalidStrategy1: 'not a function', - invalidStrategy2: null, - invalidStrategy3: undefined, - invalidStrategy4: {}, - invalidStrategy5: [], - }, - }) - - expect(mixedHelper._lookupCustomLocator('validStrategy')).to.be.a('function') - expect(mixedHelper._lookupCustomLocator('invalidStrategy1')).to.be.null - expect(mixedHelper._lookupCustomLocator('invalidStrategy2')).to.be.null - expect(mixedHelper._lookupCustomLocator('invalidStrategy3')).to.be.null - expect(mixedHelper._lookupCustomLocator('invalidStrategy4')).to.be.null - expect(mixedHelper._lookupCustomLocator('invalidStrategy5')).to.be.null - }) - }) - - describe('buildLocatorString integration', () => { - it('should build correct locator strings for custom strategies', () => { - // We can't easily test buildLocatorString directly since it's not exported, - // but we can test that custom locators are properly identified - const locator1 = new Locator({ byRole: 'button' }) - const locator2 = new Locator({ byTestId: 'submit' }) - const locator3 = new Locator({ byDataQa: 'form-section' }) - - expect(locator1.isCustom()).to.be.true - expect(locator2.isCustom()).to.be.true - expect(locator3.isCustom()).to.be.true - - expect(locator1.type).to.equal('byRole') - expect(locator1.value).to.equal('button') - - expect(locator2.type).to.equal('byTestId') - expect(locator2.value).to.equal('submit') - - expect(locator3.type).to.equal('byDataQa') - expect(locator3.value).to.equal('form-section') - }) - }) -}) diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 75a43d485..1e1b829cd 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -1189,161 +1189,6 @@ describe('Playwright', function () { }) }) -describe('Playwright - Custom Locator Strategies Configuration', () => { - let customI - - before(async () => { - // Create a new Playwright instance with custom locator strategies - customI = new Playwright({ - url: siteUrl, - browser: process.env.BROWSER || 'chromium', - show: false, - waitForTimeout: 5000, - timeout: 2000, - restart: true, - chrome: { - args: ['--no-sandbox', '--disable-setuid-sandbox'], - }, - 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 all matching elements - }, - }, - }) - // Note: Skip _init() for configuration-only tests to avoid browser dependency - // await customI._init() - // Skip browser initialization for basic config tests - }) - - after(async () => { - if (customI) { - try { - await customI._after() - } catch (e) { - // Ignore cleanup errors if browser wasn't initialized - } - } - }) - - it('should have custom locator strategies defined', () => { - expect(customI.customLocatorStrategies).to.not.be.undefined - expect(customI.customLocatorStrategies.byRole).to.be.a('function') - expect(customI.customLocatorStrategies.byTestId).to.be.a('function') - expect(customI.customLocatorStrategies.byDataQa).to.be.a('function') - }) - - it('should detect custom locator strategies are defined', () => { - expect(customI._isCustomLocatorStrategyDefined()).to.be.true - }) - - it('should lookup custom locator functions', () => { - const byRoleFunction = customI._lookupCustomLocator('byRole') - expect(byRoleFunction).to.be.a('function') - - const nonExistentFunction = customI._lookupCustomLocator('nonExistent') - expect(nonExistentFunction).to.be.null - }) - - it('should identify custom locators correctly', () => { - const customLocator = { byRole: 'button' } - expect(customI._isCustomLocator(customLocator)).to.be.true - - const standardLocator = { css: '#test' } - expect(customI._isCustomLocator(standardLocator)).to.be.false - }) - - it('should throw error for undefined custom locator strategy', () => { - const invalidLocator = { nonExistent: 'test' } - - try { - customI._isCustomLocator(invalidLocator) - expect.fail('Should have thrown an error') - } catch (error) { - expect(error.message).to.include('Please define "customLocatorStrategies"') - } - }) -}) - -describe('Playwright - Custom Locator Strategies Browser Tests', () => { - let customI - - before(async () => { - // Create a new Playwright instance with custom locator strategies - customI = new Playwright({ - url: siteUrl, - browser: process.env.BROWSER || 'chromium', - show: false, - waitForTimeout: 5000, - timeout: 2000, - restart: true, - chrome: { - args: ['--no-sandbox', '--disable-setuid-sandbox'], - }, - 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 all matching elements - }, - }, - }) - await customI._init() - }) - - after(async () => { - if (customI) { - try { - await customI._after() - } catch (e) { - // Ignore cleanup errors if browser wasn't initialized - } - } - }) - - it('should use custom locator to find elements on page', async function () { - // Skip if browser can't be initialized - try { - await customI._beforeSuite() - await customI._before() - } catch (e) { - this.skip() // Skip if browser not available - } - - await customI.amOnPage('/form/example1') - - // Test byRole locator - assuming the page has elements with role attributes - // This test assumes there's a button with role="button" on the form page - // If the test fails, it means the page doesn't have the expected elements - // but the custom locator mechanism is working if no errors are thrown - - try { - const elements = await customI._locate({ byRole: 'button' }) - // If we get here without error, the custom locator is working - expect(elements).to.be.an('array') - } catch (error) { - // If the error is about element not found, that's ok - means locator works but element doesn't exist - // If it's about custom locator not being recognized, that's a real failure - if (error.message.includes('Please define "customLocatorStrategies"')) { - throw error - } - // Element not found is acceptable - means the custom locator is working - console.log('Custom locator working but element not found (expected):', error.message) - } - }) -}) - let remoteBrowser async function createRemoteBrowser() { if (remoteBrowser) {