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
52 changes: 52 additions & 0 deletions lib/element/WebElement.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from 'assert'
import { simplifyHtmlElement } from '../html.js'

/**
* Unified WebElement class that wraps native element instances from different helpers
Expand Down Expand Up @@ -306,6 +307,57 @@ 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}`)
}
}

async toSimplifiedHTML(maxLength = 300) {
const outerHTML = await this.toOuterHTML()
return simplifyHtmlElement(outerHTML, maxLength)
}

_normalizeLocator(locator) {
if (typeof locator === 'string') {
return locator
Expand Down
27 changes: 14 additions & 13 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

Expand All @@ -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]
}

Expand Down Expand Up @@ -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()
Expand All @@ -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]

Expand All @@ -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 })
Expand Down Expand Up @@ -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
}

Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
}

Expand Down
21 changes: 21 additions & 0 deletions lib/helper/Puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -270,6 +271,7 @@ class Puppeteer extends Helper {
show: false,
defaultPopupAction: 'accept',
highlightElement: false,
strict: false,
}

return Object.assign(defaults, config)
Expand Down Expand Up @@ -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)
}

Expand All @@ -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]
}

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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 = []
Expand Down
16 changes: 16 additions & 0 deletions lib/helper/WebDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -503,6 +504,7 @@ class WebDriver extends Helper {
keepBrowserState: false,
deprecationWarnings: false,
highlightElement: false,
strict: false,
}

// override defaults with config
Expand Down Expand Up @@ -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))
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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 {
Expand All @@ -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())
Expand All @@ -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))
Expand Down Expand Up @@ -3289,6 +3298,13 @@ function usingFirstElement(els) {
return els[0]
}

function assertOnlyOneElement(elements, locator, helper) {
if (elements.length > 1) {
const webElements = Array.from(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
Expand Down
Loading