From c670e495b8f69bcf10c130ba8a0254ce2afbf1a2 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 17 Mar 2026 03:24:18 +0200 Subject: [PATCH 1/3] feat: improve MultipleElementsFound error and add strict mode to all helpers Add strict mode support to Puppeteer and WebDriver helpers (previously only Playwright). When `strict: true`, single-element operations (click, fillField, etc.) throw MultipleElementsFound if more than one element matches. Refactor MultipleElementsFound to use WebElement[] with async fetchDetails() that shows absolute XPath and minified outerHTML for each matched element. Add toAbsoluteXPath() and toOuterHTML() to WebElement class. Auto-call fetchDetails() in CLI error display. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/element/WebElement.js | 46 +++++++ lib/helper/Playwright.js | 27 ++-- lib/helper/Puppeteer.js | 21 +++ lib/helper/WebDriver.js | 16 +++ lib/helper/errors/MultipleElementsFound.js | 147 ++++++--------------- lib/mocha/cli.js | 10 ++ test/helper/webapi.js | 54 ++++++++ 7 files changed, 198 insertions(+), 123 deletions(-) diff --git a/lib/element/WebElement.js b/lib/element/WebElement.js index 6e8cec431..a59e35fb2 100644 --- a/lib/element/WebElement.js +++ b/lib/element/WebElement.js @@ -306,6 +306,52 @@ class WebElement { * @returns {string} Normalized CSS selector * @private */ + async toAbsoluteXPath() { + const xpathFn = (el) => { + const parts = [] + let current = el + while (current && current.nodeType === Node.ELEMENT_NODE) { + let index = 0 + let sibling = current.previousSibling + while (sibling) { + if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) { + index++ + } + sibling = sibling.previousSibling + } + const tagName = current.tagName.toLowerCase() + const pathIndex = index > 0 ? `[${index + 1}]` : '' + parts.unshift(`${tagName}${pathIndex}`) + current = current.parentElement + } + return '/' + parts.join('/') + } + + switch (this.helperType) { + case 'playwright': + return this.element.evaluate(xpathFn) + case 'puppeteer': + return this.element.evaluate(xpathFn) + case 'webdriver': + return this.helper.browser.execute(xpathFn, this.element) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + async toOuterHTML() { + switch (this.helperType) { + case 'playwright': + return this.element.evaluate(el => el.outerHTML) + case 'puppeteer': + return this.element.evaluate(el => el.outerHTML) + case 'webdriver': + return this.helper.browser.execute(el => el.outerHTML, this.element) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + _normalizeLocator(locator) { if (typeof locator === 'string') { return locator diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 6798cbc75..3948bd81b 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1763,7 +1763,7 @@ class Playwright extends Helper { if (elements.length === 0) { throw new ElementNotFound(locator, 'Element', 'was not found') } - if (this.options.strict) assertOnlyOneElement(elements, locator) + if (this.options.strict) assertOnlyOneElement(elements, locator, this) return elements[0] } @@ -1779,7 +1779,7 @@ class Playwright extends Helper { const context = providedContext || (await this._getContext()) const els = await findCheckable.call(this, locator, context) assertElementExists(els[0], locator, 'Checkbox or radio') - if (this.options.strict) assertOnlyOneElement(els, locator) + if (this.options.strict) assertOnlyOneElement(els, locator, this) return els[0] } @@ -2266,7 +2266,7 @@ class Playwright extends Helper { async fillField(field, value, context = null) { const els = await findFields.call(this, field, context) assertElementExists(els, field, 'Field') - if (this.options.strict) assertOnlyOneElement(els, field) + if (this.options.strict) assertOnlyOneElement(els, field, this) const el = els[0] await el.clear() @@ -2285,7 +2285,7 @@ class Playwright extends Helper { async clearField(locator, context = null) { const els = await findFields.call(this, locator, context) assertElementExists(els, locator, 'Field to clear') - if (this.options.strict) assertOnlyOneElement(els, locator) + if (this.options.strict) assertOnlyOneElement(els, locator, this) const el = els[0] @@ -2302,7 +2302,7 @@ class Playwright extends Helper { async appendField(field, value, context = null) { const els = await findFields.call(this, field, context) assertElementExists(els, field, 'Field') - if (this.options.strict) assertOnlyOneElement(els, field) + if (this.options.strict) assertOnlyOneElement(els, field, this) await highlightActiveElement.call(this, els[0]) await els[0].press('End') await els[0].type(value.toString(), { delay: this.options.pressKeyDelay }) @@ -4300,7 +4300,7 @@ async function findClickable(matcher, locator) { if (!matchedLocator.isFuzzy()) { const els = await findElements.call(this, matcher, matchedLocator) - if (this.options.strict) assertOnlyOneElement(els, locator) + if (this.options.strict) assertOnlyOneElement(els, locator, this) return els } @@ -4310,7 +4310,7 @@ async function findClickable(matcher, locator) { try { els = await matcher.getByRole('button', { name: matchedLocator.value }).all() if (els.length) { - if (this.options.strict) assertOnlyOneElement(els, locator) + if (this.options.strict) assertOnlyOneElement(els, locator, this) return els } } catch (err) { @@ -4320,7 +4320,7 @@ async function findClickable(matcher, locator) { try { els = await matcher.getByRole('link', { name: matchedLocator.value }).all() if (els.length) { - if (this.options.strict) assertOnlyOneElement(els, locator) + if (this.options.strict) assertOnlyOneElement(els, locator, this) return els } } catch (err) { @@ -4329,20 +4329,20 @@ async function findClickable(matcher, locator) { els = await findElements.call(this, matcher, Locator.clickable.narrow(literal)) if (els.length) { - if (this.options.strict) assertOnlyOneElement(els, locator) + if (this.options.strict) assertOnlyOneElement(els, locator, this) return els } els = await findElements.call(this, matcher, Locator.clickable.wide(literal)) if (els.length) { - if (this.options.strict) assertOnlyOneElement(els, locator) + if (this.options.strict) assertOnlyOneElement(els, locator, this) return els } try { els = await findElements.call(this, matcher, Locator.clickable.self(literal)) if (els.length) { - if (this.options.strict) assertOnlyOneElement(els, locator) + if (this.options.strict) assertOnlyOneElement(els, locator, this) return els } } catch (err) { @@ -4619,9 +4619,10 @@ function assertElementExists(res, locator, prefix, suffix) { } } -function assertOnlyOneElement(elements, locator) { +function assertOnlyOneElement(elements, locator, helper) { if (elements.length > 1) { - throw new MultipleElementsFound(locator, elements) + const webElements = elements.map(el => new WebElement(el, helper)) + throw new MultipleElementsFound(locator, webElements) } } diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 12f07f13f..aa8f557f2 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -33,6 +33,7 @@ import { } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' +import MultipleElementsFound from './errors/MultipleElementsFound.js' import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js' import Popup from './extras/Popup.js' import Console from './extras/Console.js' @@ -270,6 +271,7 @@ class Puppeteer extends Helper { show: false, defaultPopupAction: 'accept', highlightElement: false, + strict: false, } return Object.assign(defaults, config) @@ -988,6 +990,14 @@ class Puppeteer extends Helper { */ async _locateElement(locator) { const context = await this.context + if (this.options.strict) { + const elements = await findElements.call(this, context, locator) + if (elements.length === 0) { + throw new ElementNotFound(locator, 'Element', 'was not found') + } + assertOnlyOneElement(elements, locator, this) + return elements[0] + } return findElement.call(this, context, locator) } @@ -1005,6 +1015,7 @@ class Puppeteer extends Helper { if (!els || els.length === 0) { throw new ElementNotFound(locator, 'Checkbox or radio') } + if (this.options.strict) assertOnlyOneElement(els, locator, this) return els[0] } @@ -1564,6 +1575,7 @@ class Puppeteer extends Helper { async fillField(field, value, context = null) { const els = await findVisibleFields.call(this, field, context) assertElementExists(els, field, 'Field') + if (this.options.strict) assertOnlyOneElement(els, field, this) const el = els[0] const tag = await el.getProperty('tagName').then(el => el.jsonValue()) const editable = await el.getProperty('contenteditable').then(el => el.jsonValue()) @@ -1594,6 +1606,7 @@ class Puppeteer extends Helper { async appendField(field, value, context = null) { const els = await findVisibleFields.call(this, field, context) assertElementExists(els, field, 'Field') + if (this.options.strict) assertOnlyOneElement(els, field, this) highlightActiveElement.call(this, els[0], await this._getContext()) await els[0].press('End') await els[0].type(value.toString(), { delay: this.options.pressKeyDelay }) @@ -3098,6 +3111,7 @@ async function proceedClick(locator, context = null, options = {}) { } else { assertElementExists(els, locator, 'Clickable element') } + if (this.options.strict) assertOnlyOneElement(els, locator, this) highlightActiveElement.call(this, els[0], await this._getContext()) @@ -3425,6 +3439,13 @@ function assertElementExists(res, locator, prefix, suffix) { } } +function assertOnlyOneElement(elements, locator, helper) { + if (elements.length > 1) { + const webElements = elements.map(el => new WebElement(el, helper)) + throw new MultipleElementsFound(locator, webElements) + } +} + function $XPath(element, selector) { const found = document.evaluate(selector, element || document.body, null, 5, null) const res = [] diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index c16121157..3cb653d0b 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -30,6 +30,7 @@ import { } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' +import MultipleElementsFound from './errors/MultipleElementsFound.js' import ConnectionRefused from './errors/ConnectionRefused.js' import Locator from '../locator.js' import { highlightElement } from './scripts/highlightElement.js' @@ -503,6 +504,7 @@ class WebDriver extends Helper { keepBrowserState: false, deprecationWarnings: false, highlightElement: false, + strict: false, } // override defaults with config @@ -1090,6 +1092,7 @@ class WebDriver extends Helper { } else { assertElementExists(res, locator, 'Clickable element') } + if (this.options.strict) assertOnlyOneElement(res, locator, this) const elem = usingFirstElement(res) highlightActiveElement.call(this, elem) return this.browser[clickMethod](getElementId(elem)) @@ -1109,6 +1112,7 @@ class WebDriver extends Helper { } else { assertElementExists(res, locator, 'Clickable element') } + if (this.options.strict) assertOnlyOneElement(res, locator, this) const elem = usingFirstElement(res) highlightActiveElement.call(this, elem) @@ -1136,6 +1140,7 @@ class WebDriver extends Helper { } else { assertElementExists(res, locator, 'Clickable element') } + if (this.options.strict) assertOnlyOneElement(res, locator, this) const elem = usingFirstElement(res) highlightActiveElement.call(this, elem) @@ -1156,6 +1161,7 @@ class WebDriver extends Helper { } else { assertElementExists(res, locator, 'Clickable element') } + if (this.options.strict) assertOnlyOneElement(res, locator, this) const el = usingFirstElement(res) @@ -1272,6 +1278,7 @@ class WebDriver extends Helper { async fillField(field, value, context = null) { const res = await findFields.call(this, field, context) assertElementExists(res, field, 'Field') + if (this.options.strict) assertOnlyOneElement(res, field, this) const elem = usingFirstElement(res) highlightActiveElement.call(this, elem) try { @@ -1295,6 +1302,7 @@ class WebDriver extends Helper { async appendField(field, value, context = null) { const res = await findFields.call(this, field, context) assertElementExists(res, field, 'Field') + if (this.options.strict) assertOnlyOneElement(res, field, this) const elem = usingFirstElement(res) highlightActiveElement.call(this, elem) return elem.addValue(value.toString()) @@ -1307,6 +1315,7 @@ class WebDriver extends Helper { async clearField(field, context = null) { const res = await findFields.call(this, field, context) assertElementExists(res, field, 'Field') + if (this.options.strict) assertOnlyOneElement(res, field, this) const elem = usingFirstElement(res) highlightActiveElement.call(this, elem) return elem.clearValue(getElementId(elem)) @@ -3289,6 +3298,13 @@ function usingFirstElement(els) { return els[0] } +function assertOnlyOneElement(elements, locator, helper) { + if (elements.length > 1) { + const webElements = elements.map(el => new WebElement(el, helper)) + throw new MultipleElementsFound(locator, webElements) + } +} + function getElementId(el) { // W3C WebDriver web element identifier // https://w3c.github.io/webdriver/#dfn-web-element-identifier diff --git a/lib/helper/errors/MultipleElementsFound.js b/lib/helper/errors/MultipleElementsFound.js index 46c16801b..0fa3a13bc 100644 --- a/lib/helper/errors/MultipleElementsFound.js +++ b/lib/helper/errors/MultipleElementsFound.js @@ -1,40 +1,55 @@ import Locator from '../../locator.js' +import { removeNonInteractiveElements } from '../../html.js' -/** - * Error thrown when strict mode is enabled and multiple elements are found - * for a single-element locator operation (click, fillField, etc.) - */ class MultipleElementsFound extends Error { - /** - * @param {Locator|string|object} locator - The locator used - * @param {Array} elements - Array of Playwright element handles found - */ - constructor(locator, elements) { - super(`Multiple elements (${elements.length}) found for "${locator}". Call fetchDetails() for full information.`) + constructor(locator, webElements) { + const locatorStr = (typeof locator === 'object' && !(locator instanceof Locator)) + ? new Locator(locator).toString() + : String(locator) + super(`Multiple elements (${webElements.length}) found for "${locatorStr}" in strict mode. Call fetchDetails() for full information.`) this.name = 'MultipleElementsFound' this.locator = locator - this.elements = elements - this.count = elements.length + this.webElements = webElements + this.count = webElements.length this._detailsFetched = false } - /** - * Fetch detailed information about the found elements asynchronously - * This updates the error message with XPath and element previews - */ async fetchDetails() { if (this._detailsFetched) return try { - if (typeof this.locator === 'object' && !(this.locator instanceof Locator)) { - this.locator = JSON.stringify(this.locator) + const items = [] + const maxToShow = Math.min(this.count, 10) + + for (let i = 0; i < maxToShow; i++) { + const webEl = this.webElements[i] + try { + const xpath = await webEl.toAbsoluteXPath() + let outerHTML = await webEl.toOuterHTML() + try { + outerHTML = removeNonInteractiveElements(outerHTML) + outerHTML = outerHTML.replace(/<\/head>(.*)<\/body><\/html>/s, '$1').trim() + } catch (e) { + // keep raw outerHTML if minification fails + } + if (outerHTML.length > 300) { + outerHTML = outerHTML.slice(0, 300) + '...' + } + items.push(` ${i + 1}. > ${xpath}\n ${outerHTML}`) + } catch (err) { + items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`) + } } - const locatorObj = new Locator(this.locator) - const elementList = await this._generateElementList(this.elements, this.count) + if (this.count > 10) { + items.push(` ... and ${this.count - 10} more`) + } - this.message = `Multiple elements (${this.count}) found for "${locatorObj.toString()}" in strict mode.\n` + - elementList + + const locatorStr = (typeof this.locator === 'object' && !(this.locator instanceof Locator)) + ? new Locator(this.locator).toString() + : String(this.locator) + this.message = `Multiple elements (${this.count}) found for "${locatorStr}" in strict mode.\n` + + items.join('\n') + `\nUse a more specific locator or use grabWebElements() to handle multiple elements.` } catch (err) { this.message = `Multiple elements (${this.count}) found. Failed to fetch details: ${err.message}` @@ -42,94 +57,6 @@ class MultipleElementsFound extends Error { this._detailsFetched = true } - - /** - * Generate a formatted list of found elements with their XPath and preview - * @param {Array} elements - * @param {number} count - * @returns {Promise} - */ - async _generateElementList(elements, count) { - const items = [] - const maxToShow = Math.min(count, 10) - - for (let i = 0; i < maxToShow; i++) { - const el = elements[i] - try { - const info = await this._getElementInfo(el) - items.push(` ${i + 1}. ${info.xpath} (${info.preview})`) - } catch (err) { - // Element might be detached or inaccessible - items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`) - } - } - - if (count > 10) { - items.push(` ... and ${count - 10} more`) - } - - return items.join('\n') - } - - /** - * Get XPath and preview for an element by running JavaScript in browser context - * @param {HTMLElement} element - * @returns {Promise<{xpath: string, preview: string}>} - */ - async _getElementInfo(element) { - return element.evaluate((el) => { - // Generate a unique XPath for this element - const getUniqueXPath = (element) => { - if (element.id) { - return `//*[@id="${element.id}"]` - } - - const parts = [] - let current = element - - while (current && current.nodeType === Node.ELEMENT_NODE) { - let index = 0 - let sibling = current.previousSibling - - while (sibling) { - if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) { - index++ - } - sibling = sibling.previousSibling - } - - const tagName = current.tagName.toLowerCase() - const pathIndex = index > 0 ? `[${index + 1}]` : '' - parts.unshift(`${tagName}${pathIndex}`) - - current = current.parentElement - - // Stop at body to keep XPath reasonable - if (current && current.tagName === 'BODY') { - parts.unshift('body') - break - } - } - - return '/' + parts.join('/') - } - - // Get a preview of the element (tag, classes, id) - const getPreview = (element) => { - const tag = element.tagName.toLowerCase() - const id = element.id ? `#${element.id}` : '' - const classes = element.className - ? '.' + element.className.split(' ').filter(c => c).join('.') - : '' - return `${tag}${id}${classes || ''}` - } - - return { - xpath: getUniqueXPath(el), - preview: getPreview(el), - } - }) - } } export default MultipleElementsFound diff --git a/lib/mocha/cli.js b/lib/mocha/cli.js index d905643e7..bb08e0caa 100644 --- a/lib/mocha/cli.js +++ b/lib/mocha/cli.js @@ -202,6 +202,16 @@ class Cli extends Base { // failures if (stats.failures) { + for (const test of this.failures) { + if (test.err && typeof test.err.fetchDetails === 'function') { + try { + await test.err.fetchDetails() + } catch (e) { + // ignore fetch errors + } + } + } + // append step traces this.failures = this.failures.map(test => { // we will change the stack trace, so we need to clone the test diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 45e75042a..9f7d8913e 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -2278,4 +2278,58 @@ export function tests() { await I.see('Another User') }) }) + + describe('#strict mode', () => { + afterEach(() => { + I.options.strict = false + }) + + it('should throw error if multiple elements found for click', async () => { + await I.amOnPage('/info') + I.options.strict = true + let err + try { + await I.click('#grab-multiple a') + } catch (e) { + err = e + } + expect(err).to.exist + expect(err.constructor.name).to.equal('MultipleElementsFound') + expect(err.message).to.include('Multiple elements') + }) + + it('should throw error if multiple elements found for fillField', async () => { + await I.amOnPage('/form/example20') + I.options.strict = true + let err + try { + await I.fillField("input[name='txtName']", 'test') + } catch (e) { + err = e + } + expect(err).to.exist + expect(err.constructor.name).to.equal('MultipleElementsFound') + }) + + it('should not throw error if only one element found', async () => { + await I.amOnPage('/info') + I.options.strict = true + await I.click('#first-link') + }) + + it('should provide element details after fetchDetails', async () => { + await I.amOnPage('/info') + I.options.strict = true + let err + try { + await I.click('#grab-multiple a') + } catch (e) { + err = e + } + expect(err).to.exist + await err.fetchDetails() + expect(err.message).to.include('/html') + expect(err.message).to.include('Use a more specific locator') + }) + }) } From ca7ba6b70b8b2d71e688d705afda92116b1842be Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 17 Mar 2026 03:30:33 +0200 Subject: [PATCH 2/3] refactor: extract simplifyHtmlElement to lib/html.js and add toSimplifiedHTML to WebElement Move HTML simplification logic (removeNonInteractiveElements + unwrap + truncate) into a reusable simplifyHtmlElement() function in lib/html.js. Add toSimplifiedHTML() method to WebElement that combines toOuterHTML() with simplifyHtmlElement(). MultipleElementsFound.fetchDetails() now uses webEl.toSimplifiedHTML() directly. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/element/WebElement.js | 6 ++++++ lib/helper/errors/MultipleElementsFound.js | 14 ++------------ lib/html.js | 15 ++++++++++++++- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/element/WebElement.js b/lib/element/WebElement.js index a59e35fb2..4c30028b2 100644 --- a/lib/element/WebElement.js +++ b/lib/element/WebElement.js @@ -1,4 +1,5 @@ import assert from 'assert' +import { simplifyHtmlElement } from '../html.js' /** * Unified WebElement class that wraps native element instances from different helpers @@ -352,6 +353,11 @@ class WebElement { } } + async toSimplifiedHTML(maxLength = 300) { + const outerHTML = await this.toOuterHTML() + return simplifyHtmlElement(outerHTML, maxLength) + } + _normalizeLocator(locator) { if (typeof locator === 'string') { return locator diff --git a/lib/helper/errors/MultipleElementsFound.js b/lib/helper/errors/MultipleElementsFound.js index 0fa3a13bc..4207fa7dd 100644 --- a/lib/helper/errors/MultipleElementsFound.js +++ b/lib/helper/errors/MultipleElementsFound.js @@ -1,5 +1,4 @@ import Locator from '../../locator.js' -import { removeNonInteractiveElements } from '../../html.js' class MultipleElementsFound extends Error { constructor(locator, webElements) { @@ -25,17 +24,8 @@ class MultipleElementsFound extends Error { const webEl = this.webElements[i] try { const xpath = await webEl.toAbsoluteXPath() - let outerHTML = await webEl.toOuterHTML() - try { - outerHTML = removeNonInteractiveElements(outerHTML) - outerHTML = outerHTML.replace(/<\/head>(.*)<\/body><\/html>/s, '$1').trim() - } catch (e) { - // keep raw outerHTML if minification fails - } - if (outerHTML.length > 300) { - outerHTML = outerHTML.slice(0, 300) + '...' - } - items.push(` ${i + 1}. > ${xpath}\n ${outerHTML}`) + const html = await webEl.toSimplifiedHTML() + items.push(` ${i + 1}. > ${xpath}\n ${html}`) } catch (err) { items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`) } diff --git a/lib/html.js b/lib/html.js index 1257a790f..d1d74b1a0 100644 --- a/lib/html.js +++ b/lib/html.js @@ -245,4 +245,17 @@ function splitByChunks(text, chunkSize) { return chunks.map(chunk => chunk.trim()) } -export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml } +function simplifyHtmlElement(html, maxLength = 300) { + try { + html = removeNonInteractiveElements(html) + html = html.replace(/<\/head>(.*)<\/body><\/html>/s, '$1').trim() + } catch (e) { + // keep raw html if minification fails + } + if (html.length > maxLength) { + html = html.slice(0, maxLength) + '...' + } + return html +} + +export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement } From 5d53aced5ce1f97044f050b1942189dc02c3af81 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 17 Mar 2026 03:49:24 +0200 Subject: [PATCH 3/3] fix: WebDriver strict mode - use Array.from for element collections and fix HTML unwrap regex WebdriverIO $$ returns a special collection, not a plain array. Using Array.from() before .map() ensures WebElement[] is a real array with .length. Also fix simplifyHtmlElement regex to handle cases where tag is absent in the parse5 output. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/helper/WebDriver.js | 2 +- lib/html.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 3cb653d0b..4f25098e7 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -3300,7 +3300,7 @@ function usingFirstElement(els) { function assertOnlyOneElement(elements, locator, helper) { if (elements.length > 1) { - const webElements = elements.map(el => new WebElement(el, helper)) + const webElements = Array.from(elements).map(el => new WebElement(el, helper)) throw new MultipleElementsFound(locator, webElements) } } diff --git a/lib/html.js b/lib/html.js index d1d74b1a0..6edaef3f4 100644 --- a/lib/html.js +++ b/lib/html.js @@ -248,7 +248,7 @@ function splitByChunks(text, chunkSize) { function simplifyHtmlElement(html, maxLength = 300) { try { html = removeNonInteractiveElements(html) - html = html.replace(/<\/head>(.*)<\/body><\/html>/s, '$1').trim() + html = html.replace(/(?:.*?<\/head>)?(.*)<\/body><\/html>/s, '$1').trim() } catch (e) { // keep raw html if minification fails }