Skip to content

Commit 8e5298c

Browse files
DavertMikclaude
andcommitted
feat: warn when a text is passed where a selector is expected
Methods like waitForElement, seeElement and grabTextFrom expect a CSS or XPath locator and, unlike click or fillField, do not fall back to searching by text. A sentence passed to them is matched as CSS, finds nothing, and the step fails on timeout with a message that says nothing about the real cause. Adds a heuristic that recognises such strings: several words, no CSS or XPath punctuation, not a chain of tag names. Wired into 32 locator-only methods across Playwright, Puppeteer and WebDriver. In debug mode it prints a [Warning] with a suggestion for the method used; with strict: true it throws InvalidSelector so the test fails immediately instead of after the full timeout. Valid locators with spaces are left alone: `div span`, `my-app my-button`, `text=Save Changes` and `~accessibility id` all pass the check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NSR3yk8NgMFkPSspsynKUN
1 parent eb1bcdc commit 8e5298c

7 files changed

Lines changed: 294 additions & 0 deletions

File tree

docs/element-selection.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,33 @@ And when you know there are multiple matches and want a specific one, `elementIn
115115
I.click('a', step.opts({ elementIndex: 2 }))
116116
```
117117

118+
## Text Passed Instead of a Selector
119+
120+
`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:
121+
122+
```js
123+
I.waitForElement('Description Persistence Suite') // waits 10s, then "still not present on page"
124+
```
125+
126+
CodeceptJS detects this and warns when the run is in debug mode:
127+
128+
```
129+
I wait for element "Description Persistence Suite"
130+
› [Warning] "Description Persistence Suite" doesn't look like a CSS or XPath selector.
131+
I.waitForElement() expects an element locator, so this text is matched as CSS
132+
and finds nothing. Use I.waitForText() to wait for a text on page.
133+
```
134+
135+
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.
136+
137+
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.
138+
118139
## Summary
119140

120141
| Situation | Approach |
121142
|-----------|----------|
122143
| You want to catch ambiguous locators early | Enable `strict: true` in helper config |
144+
| You passed a text where a selector is expected | Run with `--debug` for the warning, or `strict: true` to fail fast |
123145
| You need a specific element from a known list | Use `step.opts({ elementIndex: N })` |
124146
| You want to iterate over all matching elements | Use [`eachElement`](/els) from the `els` module |
125147
| You need full control over element inspection | Use [`grabWebElements`](/WebElement) to get all matches |

lib/helper/Playwright.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { setRestartStrategy, restartsSession, restartsContext, restartsBrowser }
5555
import { createValueEngine, createDisabledEngine } from './extras/PlaywrightPropEngine.js'
5656
import { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
5757
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
58+
import { checkSelectorIsNotText } from './extras/selectorCheck.js'
5859

5960
const pathSeparator = path.sep
6061

@@ -1499,6 +1500,7 @@ class Playwright extends Helper {
14991500
*
15001501
*/
15011502
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
1503+
checkSelectorIsNotText(this, locator)
15021504
let context = null
15031505
if (typeof offsetX !== 'number') {
15041506
context = offsetX
@@ -1672,6 +1674,7 @@ class Playwright extends Helper {
16721674
* {{> scrollTo }}
16731675
*/
16741676
async scrollTo(locator, offsetX = 0, offsetY = 0) {
1677+
checkSelectorIsNotText(this, locator)
16751678
if (typeof locator === 'number' && typeof offsetX === 'number') {
16761679
offsetY = offsetX
16771680
offsetX = locator
@@ -1829,6 +1832,7 @@ class Playwright extends Helper {
18291832
*
18301833
*/
18311834
async grabWebElements(locator) {
1835+
checkSelectorIsNotText(this, locator)
18321836
const elements = await this._locate(locator)
18331837
return elements.map(element => new WebElement(element, this))
18341838
}
@@ -1838,6 +1842,7 @@ class Playwright extends Helper {
18381842
*
18391843
*/
18401844
async grabWebElement(locator) {
1845+
checkSelectorIsNotText(this, locator)
18411846
const element = await this._locateElement(locator)
18421847
return new WebElement(element, this)
18431848
}
@@ -1967,6 +1972,7 @@ class Playwright extends Helper {
19671972
*
19681973
*/
19691974
async seeElement(locator, context = null) {
1975+
checkSelectorIsNotText(this, locator)
19701976
let els
19711977
if (context) {
19721978
const contextEls = await this._locate(context)
@@ -1988,6 +1994,7 @@ class Playwright extends Helper {
19881994
*
19891995
*/
19901996
async dontSeeElement(locator, context = null) {
1997+
checkSelectorIsNotText(this, locator)
19911998
let els
19921999
if (context) {
19932000
const contextEls = await this._locate(context)
@@ -2008,6 +2015,7 @@ class Playwright extends Helper {
20082015
* {{> seeElementInDOM }}
20092016
*/
20102017
async seeElementInDOM(locator) {
2018+
checkSelectorIsNotText(this, locator)
20112019
const els = await this._locate(locator)
20122020
try {
20132021
return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
@@ -2020,6 +2028,7 @@ class Playwright extends Helper {
20202028
* {{> dontSeeElementInDOM }}
20212029
*/
20222030
async dontSeeElementInDOM(locator) {
2031+
checkSelectorIsNotText(this, locator)
20232032
const els = await this._locate(locator)
20242033
try {
20252034
return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
@@ -2415,6 +2424,7 @@ class Playwright extends Helper {
24152424
*
24162425
*/
24172426
async grabNumberOfVisibleElements(locator) {
2427+
checkSelectorIsNotText(this, locator)
24182428
let els = await this._locate(locator)
24192429
els = await Promise.all(els.map(el => el.isVisible()))
24202430
return els.filter(v => v).length
@@ -2546,6 +2556,7 @@ class Playwright extends Helper {
25462556
*
25472557
*/
25482558
async seeNumberOfElements(locator, num) {
2559+
checkSelectorIsNotText(this, locator)
25492560
const elements = await this._locate(locator)
25502561
return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
25512562
}
@@ -2556,6 +2567,7 @@ class Playwright extends Helper {
25562567
*
25572568
*/
25582569
async seeNumberOfVisibleElements(locator, num) {
2570+
checkSelectorIsNotText(this, locator)
25592571
const res = await this.grabNumberOfVisibleElements(locator)
25602572
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
25612573
}
@@ -2704,6 +2716,7 @@ class Playwright extends Helper {
27042716
*
27052717
*/
27062718
async grabTextFrom(locator) {
2719+
checkSelectorIsNotText(this, locator)
27072720
const roleElements = await handleRoleLocator(this.page, locator)
27082721
if (roleElements && roleElements.length > 0) {
27092722
const text = await roleElements[0].textContent()
@@ -2734,6 +2747,7 @@ class Playwright extends Helper {
27342747
*
27352748
*/
27362749
async grabTextFromAll(locator) {
2750+
checkSelectorIsNotText(this, locator)
27372751
const els = await this._locate(locator)
27382752
const texts = []
27392753
for (const el of els) {
@@ -2764,6 +2778,7 @@ class Playwright extends Helper {
27642778
* {{> grabHTMLFrom }}
27652779
*/
27662780
async grabHTMLFrom(locator) {
2781+
checkSelectorIsNotText(this, locator)
27672782
const html = await this.grabHTMLFromAll(locator)
27682783
assertElementExists(html, locator)
27692784
this.debugSection('HTML', html[0])
@@ -2774,6 +2789,7 @@ class Playwright extends Helper {
27742789
* {{> grabHTMLFromAll }}
27752790
*/
27762791
async grabHTMLFromAll(locator) {
2792+
checkSelectorIsNotText(this, locator)
27772793
const els = await this._locate(locator)
27782794
return Promise.all(els.map(el => el.innerHTML()))
27792795
}
@@ -2783,6 +2799,7 @@ class Playwright extends Helper {
27832799
*
27842800
*/
27852801
async grabCssPropertyFrom(locator, cssProperty) {
2802+
checkSelectorIsNotText(this, locator)
27862803
const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty)
27872804
assertElementExists(cssValues, locator)
27882805
this.debugSection('CSS', cssValues[0])
@@ -2794,6 +2811,7 @@ class Playwright extends Helper {
27942811
*
27952812
*/
27962813
async grabCssPropertyFromAll(locator, cssProperty) {
2814+
checkSelectorIsNotText(this, locator)
27972815
const els = await this._locate(locator)
27982816
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
27992817

@@ -2805,6 +2823,7 @@ class Playwright extends Helper {
28052823
*
28062824
*/
28072825
async seeCssPropertiesOnElements(locator, cssProperties) {
2826+
checkSelectorIsNotText(this, locator)
28082827
const res = await this._locate(locator)
28092828
assertElementExists(res, locator)
28102829

@@ -2840,6 +2859,7 @@ class Playwright extends Helper {
28402859
*
28412860
*/
28422861
async seeAttributesOnElements(locator, attributes) {
2862+
checkSelectorIsNotText(this, locator)
28432863
const res = await this._locate(locator)
28442864
assertElementExists(res, locator)
28452865

@@ -2893,6 +2913,7 @@ class Playwright extends Helper {
28932913
*
28942914
*/
28952915
async grabAttributeFrom(locator, attr) {
2916+
checkSelectorIsNotText(this, locator)
28962917
const attrs = await this.grabAttributeFromAll(locator, attr)
28972918
assertElementExists(attrs, locator)
28982919
this.debugSection('Attribute', attrs[0])
@@ -2904,6 +2925,7 @@ class Playwright extends Helper {
29042925
*
29052926
*/
29062927
async grabAttributeFromAll(locator, attr) {
2928+
checkSelectorIsNotText(this, locator)
29072929
const els = await this._locate(locator)
29082930
const array = []
29092931

@@ -2946,6 +2968,7 @@ class Playwright extends Helper {
29462968
*
29472969
*/
29482970
async saveElementScreenshot(locator, fileName) {
2971+
checkSelectorIsNotText(this, locator)
29492972
const outputFile = screenshotOutputFolder(fileName)
29502973

29512974
const res = await this._locateElement(locator)
@@ -3162,6 +3185,7 @@ class Playwright extends Helper {
31623185
* {{> waitForEnabled }}
31633186
*/
31643187
async waitForEnabled(locator, sec) {
3188+
checkSelectorIsNotText(this, locator)
31653189
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
31663190
locator = new Locator(locator, 'css')
31673191

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

@@ -3244,6 +3269,7 @@ class Playwright extends Helper {
32443269
*
32453270
*/
32463271
async waitNumberOfVisibleElements(locator, num, sec) {
3272+
checkSelectorIsNotText(this, locator)
32473273
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
32483274
locator = new Locator(locator, 'css')
32493275

@@ -3285,6 +3311,7 @@ class Playwright extends Helper {
32853311
*
32863312
*/
32873313
async waitForElement(locator, sec) {
3314+
checkSelectorIsNotText(this, locator)
32883315
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
32893316
locator = new Locator(locator, 'css')
32903317

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

@@ -3330,6 +3358,7 @@ class Playwright extends Helper {
33303358
* {{> waitForInvisible }}
33313359
*/
33323360
async waitForInvisible(locator, sec) {
3361+
checkSelectorIsNotText(this, locator)
33333362
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
33343363
locator = new Locator(locator, 'css')
33353364

@@ -3361,6 +3390,7 @@ class Playwright extends Helper {
33613390
* {{> waitToHide }}
33623391
*/
33633392
async waitToHide(locator, sec) {
3393+
checkSelectorIsNotText(this, locator)
33643394
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
33653395
locator = new Locator(locator, 'css')
33663396

@@ -3741,6 +3771,7 @@ class Playwright extends Helper {
37413771
* {{> waitForDetached }}
37423772
*/
37433773
async waitForDetached(locator, sec) {
3774+
checkSelectorIsNotText(this, locator)
37443775
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
37453776
locator = new Locator(locator, 'css')
37463777

@@ -3817,6 +3848,7 @@ class Playwright extends Helper {
38173848
* {{> grabElementBoundingRect }}
38183849
*/
38193850
async grabElementBoundingRect(locator, prop) {
3851+
checkSelectorIsNotText(this, locator)
38203852
const el = await this._locateElement(locator)
38213853
assertElementExists(el, locator)
38223854
const rect = await el.boundingBox()

0 commit comments

Comments
 (0)