Skip to content
Open
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
22 changes: 22 additions & 0 deletions docs/element-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,33 @@ And when you know there are multiple matches and want a specific one, `elementIn
I.click('a', step.opts({ elementIndex: 2 }))
```

## Text Passed Instead of a Selector

`waitForElement`, `seeElement`, `waitForVisible` and the rest of the wait/assert family expect a CSS or XPath locator. Unlike `click` or `fillField`, they don't fall back to searching by text. A sentence passed to them is treated as CSS, matches nothing, and the step fails with a timeout that says nothing about the real cause:

```js
I.waitForElement('Description Persistence Suite') // waits 10s, then "still not present on page"
```

CodeceptJS detects this and warns when the run is in debug mode:

```
I wait for element "Description Persistence Suite"
› [Warning] "Description Persistence Suite" doesn't look like a CSS or XPath selector.
I.waitForElement() expects an element locator, so this text is matched as CSS
and finds nothing. Use I.waitForText() to wait for a text on page.
```

With `strict: true` the same check throws `InvalidSelector` instead of warning, so the test fails immediately with a readable message rather than after the full timeout.

The check only fires on strings that can't be a selector: they contain a space, carry no CSS or XPath punctuation, and aren't a chain of tag names. `div span`, `my-app my-button`, `text=Save Changes` and `~accessibility id` are all left alone.

## Summary

| Situation | Approach |
|-----------|----------|
| You want to catch ambiguous locators early | Enable `strict: true` in helper config |
| You passed a text where a selector is expected | Run with `--debug` for the warning, or `strict: true` to fail fast |
| You need a specific element from a known list | Use `step.opts({ elementIndex: N })` |
| You want to iterate over all matching elements | Use [`eachElement`](/els) from the `els` module |
| You need full control over element inspection | Use [`grabWebElements`](/WebElement) to get all matches |
32 changes: 32 additions & 0 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { setRestartStrategy, restartsSession, restartsContext, restartsBrowser }
import { createValueEngine, createDisabledEngine } from './extras/PlaywrightPropEngine.js'
import { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
import { checkSelectorIsNotText } from './extras/selectorCheck.js'

const pathSeparator = path.sep

Expand Down Expand Up @@ -1499,6 +1500,7 @@ class Playwright extends Helper {
*
*/
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
checkSelectorIsNotText(this, locator)
let context = null
if (typeof offsetX !== 'number') {
context = offsetX
Expand Down Expand Up @@ -1672,6 +1674,7 @@ class Playwright extends Helper {
* {{> scrollTo }}
*/
async scrollTo(locator, offsetX = 0, offsetY = 0) {
checkSelectorIsNotText(this, locator)
if (typeof locator === 'number' && typeof offsetX === 'number') {
offsetY = offsetX
offsetX = locator
Expand Down Expand Up @@ -1829,6 +1832,7 @@ class Playwright extends Helper {
*
*/
async grabWebElements(locator) {
checkSelectorIsNotText(this, locator)
const elements = await this._locate(locator)
return elements.map(element => new WebElement(element, this))
}
Expand All @@ -1838,6 +1842,7 @@ class Playwright extends Helper {
*
*/
async grabWebElement(locator) {
checkSelectorIsNotText(this, locator)
const element = await this._locateElement(locator)
return new WebElement(element, this)
}
Expand Down Expand Up @@ -1967,6 +1972,7 @@ class Playwright extends Helper {
*
*/
async seeElement(locator, context = null) {
checkSelectorIsNotText(this, locator)
let els
if (context) {
const contextEls = await this._locate(context)
Expand All @@ -1988,6 +1994,7 @@ class Playwright extends Helper {
*
*/
async dontSeeElement(locator, context = null) {
checkSelectorIsNotText(this, locator)
let els
if (context) {
const contextEls = await this._locate(context)
Expand All @@ -2008,6 +2015,7 @@ class Playwright extends Helper {
* {{> seeElementInDOM }}
*/
async seeElementInDOM(locator) {
checkSelectorIsNotText(this, locator)
const els = await this._locate(locator)
try {
return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
Expand All @@ -2020,6 +2028,7 @@ class Playwright extends Helper {
* {{> dontSeeElementInDOM }}
*/
async dontSeeElementInDOM(locator) {
checkSelectorIsNotText(this, locator)
const els = await this._locate(locator)
try {
return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
Expand Down Expand Up @@ -2415,6 +2424,7 @@ class Playwright extends Helper {
*
*/
async grabNumberOfVisibleElements(locator) {
checkSelectorIsNotText(this, locator)
let els = await this._locate(locator)
els = await Promise.all(els.map(el => el.isVisible()))
return els.filter(v => v).length
Expand Down Expand Up @@ -2546,6 +2556,7 @@ class Playwright extends Helper {
*
*/
async seeNumberOfElements(locator, num) {
checkSelectorIsNotText(this, locator)
const elements = await this._locate(locator)
return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
}
Expand All @@ -2556,6 +2567,7 @@ class Playwright extends Helper {
*
*/
async seeNumberOfVisibleElements(locator, num) {
checkSelectorIsNotText(this, locator)
const res = await this.grabNumberOfVisibleElements(locator)
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
}
Expand Down Expand Up @@ -2704,6 +2716,7 @@ class Playwright extends Helper {
*
*/
async grabTextFrom(locator) {
checkSelectorIsNotText(this, locator)
const roleElements = await handleRoleLocator(this.page, locator)
if (roleElements && roleElements.length > 0) {
const text = await roleElements[0].textContent()
Expand Down Expand Up @@ -2734,6 +2747,7 @@ class Playwright extends Helper {
*
*/
async grabTextFromAll(locator) {
checkSelectorIsNotText(this, locator)
const els = await this._locate(locator)
const texts = []
for (const el of els) {
Expand Down Expand Up @@ -2764,6 +2778,7 @@ class Playwright extends Helper {
* {{> grabHTMLFrom }}
*/
async grabHTMLFrom(locator) {
checkSelectorIsNotText(this, locator)
const html = await this.grabHTMLFromAll(locator)
assertElementExists(html, locator)
this.debugSection('HTML', html[0])
Expand All @@ -2774,6 +2789,7 @@ class Playwright extends Helper {
* {{> grabHTMLFromAll }}
*/
async grabHTMLFromAll(locator) {
checkSelectorIsNotText(this, locator)
const els = await this._locate(locator)
return Promise.all(els.map(el => el.innerHTML()))
}
Expand All @@ -2783,6 +2799,7 @@ class Playwright extends Helper {
*
*/
async grabCssPropertyFrom(locator, cssProperty) {
checkSelectorIsNotText(this, locator)
const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty)
assertElementExists(cssValues, locator)
this.debugSection('CSS', cssValues[0])
Expand All @@ -2794,6 +2811,7 @@ class Playwright extends Helper {
*
*/
async grabCssPropertyFromAll(locator, cssProperty) {
checkSelectorIsNotText(this, locator)
const els = await this._locate(locator)
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))

Expand All @@ -2805,6 +2823,7 @@ class Playwright extends Helper {
*
*/
async seeCssPropertiesOnElements(locator, cssProperties) {
checkSelectorIsNotText(this, locator)
const res = await this._locate(locator)
assertElementExists(res, locator)

Expand Down Expand Up @@ -2840,6 +2859,7 @@ class Playwright extends Helper {
*
*/
async seeAttributesOnElements(locator, attributes) {
checkSelectorIsNotText(this, locator)
const res = await this._locate(locator)
assertElementExists(res, locator)

Expand Down Expand Up @@ -2893,6 +2913,7 @@ class Playwright extends Helper {
*
*/
async grabAttributeFrom(locator, attr) {
checkSelectorIsNotText(this, locator)
const attrs = await this.grabAttributeFromAll(locator, attr)
assertElementExists(attrs, locator)
this.debugSection('Attribute', attrs[0])
Expand All @@ -2904,6 +2925,7 @@ class Playwright extends Helper {
*
*/
async grabAttributeFromAll(locator, attr) {
checkSelectorIsNotText(this, locator)
const els = await this._locate(locator)
const array = []

Expand Down Expand Up @@ -2946,6 +2968,7 @@ class Playwright extends Helper {
*
*/
async saveElementScreenshot(locator, fileName) {
checkSelectorIsNotText(this, locator)
const outputFile = screenshotOutputFolder(fileName)

const res = await this._locateElement(locator)
Expand Down Expand Up @@ -3162,6 +3185,7 @@ class Playwright extends Helper {
* {{> waitForEnabled }}
*/
async waitForEnabled(locator, sec) {
checkSelectorIsNotText(this, locator)
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
locator = new Locator(locator, 'css')

Expand All @@ -3188,6 +3212,7 @@ class Playwright extends Helper {
* {{> waitForDisabled }}
*/
async waitForDisabled(locator, sec) {
checkSelectorIsNotText(this, locator)
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
locator = new Locator(locator, 'css')

Expand Down Expand Up @@ -3244,6 +3269,7 @@ class Playwright extends Helper {
*
*/
async waitNumberOfVisibleElements(locator, num, sec) {
checkSelectorIsNotText(this, locator)
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
locator = new Locator(locator, 'css')

Expand Down Expand Up @@ -3285,6 +3311,7 @@ class Playwright extends Helper {
*
*/
async waitForElement(locator, sec) {
checkSelectorIsNotText(this, locator)
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
locator = new Locator(locator, 'css')

Expand All @@ -3300,6 +3327,7 @@ class Playwright extends Helper {
* {{> waitForVisible }}
*/
async waitForVisible(locator, sec) {
checkSelectorIsNotText(this, locator)
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
locator = new Locator(locator, 'css')

Expand Down Expand Up @@ -3330,6 +3358,7 @@ class Playwright extends Helper {
* {{> waitForInvisible }}
*/
async waitForInvisible(locator, sec) {
checkSelectorIsNotText(this, locator)
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
locator = new Locator(locator, 'css')

Expand Down Expand Up @@ -3361,6 +3390,7 @@ class Playwright extends Helper {
* {{> waitToHide }}
*/
async waitToHide(locator, sec) {
checkSelectorIsNotText(this, locator)
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
locator = new Locator(locator, 'css')

Expand Down Expand Up @@ -3741,6 +3771,7 @@ class Playwright extends Helper {
* {{> waitForDetached }}
*/
async waitForDetached(locator, sec) {
checkSelectorIsNotText(this, locator)
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
locator = new Locator(locator, 'css')

Expand Down Expand Up @@ -3817,6 +3848,7 @@ class Playwright extends Helper {
* {{> grabElementBoundingRect }}
*/
async grabElementBoundingRect(locator, prop) {
checkSelectorIsNotText(this, locator)
const el = await this._locateElement(locator)
assertElementExists(el, locator)
const rect = await el.boundingBox()
Expand Down
Loading