@@ -27,6 +27,7 @@ import {
2727} from '../utils.js'
2828import { isColorProperty , convertColorToRGBA } from '../colorUtils.js'
2929import ElementNotFound from './errors/ElementNotFound.js'
30+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
3031import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
3132import Popup from './extras/Popup.js'
3233import Console from './extras/Console.js'
@@ -107,6 +108,9 @@ const pathSeparator = path.sep
107108 * those cookies are used instead and the configured `storageState` is ignored (no merge).
108109 * May include session cookies, auth tokens, localStorage and (if captured with
109110 * `grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit.
111+ * @prop {boolean } [strict=false] - throw error when multiple elements match a single-element locator.
112+ * When enabled, methods like `click`, `fillField`, `selectOption`, etc. will throw a
113+ * `MultipleElementsFound` error if more than one element matches the locator.
110114 */
111115const config = { }
112116
@@ -417,6 +421,7 @@ class Playwright extends Helper {
417421 highlightElement : false ,
418422 storageState : undefined ,
419423 onResponse : null ,
424+ strict : false , // Throw error when multiple elements match single-element locator
420425 }
421426
422427 process . env . testIdAttribute = 'data-testid'
@@ -1912,7 +1917,12 @@ class Playwright extends Helper {
19121917 */
19131918 async _locateElement ( locator ) {
19141919 const context = await this . _getContext ( )
1915- return findElement ( context , locator )
1920+ const elements = await findElements . call ( this , context , locator )
1921+ if ( elements . length === 0 ) {
1922+ throw new ElementNotFound ( locator , 'Element' , 'was not found' )
1923+ }
1924+ if ( this . options . strict ) assertOnlyOneElement ( elements , locator )
1925+ return elements [ 0 ]
19161926 }
19171927
19181928 /**
@@ -1927,6 +1937,7 @@ class Playwright extends Helper {
19271937 const context = providedContext || ( await this . _getContext ( ) )
19281938 const els = await findCheckable . call ( this , locator , context )
19291939 assertElementExists ( els [ 0 ] , locator , 'Checkbox or radio' )
1940+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
19301941 return els [ 0 ]
19311942 }
19321943
@@ -2399,6 +2410,7 @@ class Playwright extends Helper {
23992410 async fillField ( field , value ) {
24002411 const els = await findFields . call ( this , field )
24012412 assertElementExists ( els , field , 'Field' )
2413+ if ( this . options . strict ) assertOnlyOneElement ( els , field )
24022414 const el = els [ 0 ]
24032415
24042416 await el . clear ( )
@@ -2431,6 +2443,7 @@ class Playwright extends Helper {
24312443 async clearField ( locator , options = { } ) {
24322444 const els = await findFields . call ( this , locator )
24332445 assertElementExists ( els , locator , 'Field to clear' )
2446+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
24342447
24352448 const el = els [ 0 ]
24362449
@@ -2447,6 +2460,7 @@ class Playwright extends Helper {
24472460 async appendField ( field , value ) {
24482461 const els = await findFields . call ( this , field )
24492462 assertElementExists ( els , field , 'Field' )
2463+ if ( this . options . strict ) assertOnlyOneElement ( els , field )
24502464 await highlightActiveElement . call ( this , els [ 0 ] )
24512465 await els [ 0 ] . press ( 'End' )
24522466 await els [ 0 ] . type ( value . toString ( ) , { delay : this . options . pressKeyDelay } )
@@ -4444,20 +4458,6 @@ async function findCustomElements(matcher, locator) {
44444458 return locators
44454459}
44464460
4447- async function findElement ( matcher , locator ) {
4448- const matchedLocator = Locator . from ( locator )
4449- const roleElements = await findByRole ( matcher , matchedLocator )
4450- if ( roleElements && roleElements . length > 0 ) return roleElements [ 0 ]
4451-
4452- const isReactLocator = matchedLocator . type === 'react'
4453- const isVueLocator = matchedLocator . type === 'vue'
4454-
4455- if ( isReactLocator ) return findReact ( matcher , matchedLocator )
4456- if ( isVueLocator ) return findVue ( matcher , matchedLocator )
4457-
4458- return matcher . locator ( buildLocatorString ( matchedLocator ) ) . first ( )
4459- }
4460-
44614461async function getVisibleElements ( elements ) {
44624462 const visibleElements = [ ]
44634463 for ( const element of elements ) {
@@ -4511,20 +4511,33 @@ async function findClickable(matcher, locator) {
45114511 if ( locator . vue ) return findVue ( matcher , locator )
45124512
45134513 locator = new Locator ( locator )
4514- if ( ! locator . isFuzzy ( ) ) return findElements . call ( this , matcher , locator )
4514+ if ( ! locator . isFuzzy ( ) ) {
4515+ const els = await findElements . call ( this , matcher , locator )
4516+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4517+ return els
4518+ }
45154519
45164520 let els
45174521 const literal = xpathLocator . literal ( locator . value )
45184522
45194523 els = await findElements . call ( this , matcher , Locator . clickable . narrow ( literal ) )
4520- if ( els . length ) return els
4524+ if ( els . length ) {
4525+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4526+ return els
4527+ }
45214528
45224529 els = await findElements . call ( this , matcher , Locator . clickable . wide ( literal ) )
4523- if ( els . length ) return els
4530+ if ( els . length ) {
4531+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4532+ return els
4533+ }
45244534
45254535 try {
45264536 els = await findElements . call ( this , matcher , Locator . clickable . self ( literal ) )
4527- if ( els . length ) return els
4537+ if ( els . length ) {
4538+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4539+ return els
4540+ }
45284541 } catch ( err ) {
45294542 // Do nothing
45304543 }
@@ -4782,6 +4795,12 @@ function assertElementExists(res, locator, prefix, suffix) {
47824795 }
47834796}
47844797
4798+ function assertOnlyOneElement ( elements , locator ) {
4799+ if ( elements . length > 1 ) {
4800+ throw new MultipleElementsFound ( locator , elements )
4801+ }
4802+ }
4803+
47854804function $XPath ( element , selector ) {
47864805 const found = document . evaluate ( selector , element || document . body , null , 5 , null )
47874806 const res = [ ]
@@ -5018,7 +5037,7 @@ async function saveTraceForContext(context, name) {
50185037}
50195038
50205039async function highlightActiveElement ( element ) {
5021- if ( ( this . options . highlightElement || store . onPause ) && store . debugMode ) {
5040+ if ( this . options . highlightElement || store . onPause || store . debugMode ) {
50225041 await element . evaluate ( el => {
50235042 const prevStyle = el . style . boxShadow
50245043 el . style . boxShadow = '0px 0px 4px 3px rgba(147, 51, 234, 0.8)' // Bright purple that works on both dark/light modes
0 commit comments