@@ -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'
@@ -392,6 +393,7 @@ class Playwright extends Helper {
392393 highlightElement : false ,
393394 storageState : undefined ,
394395 onResponse : null ,
396+ strict : false ,
395397 }
396398
397399 process . env . testIdAttribute = 'data-testid'
@@ -1753,7 +1755,12 @@ class Playwright extends Helper {
17531755 */
17541756 async _locateElement ( locator ) {
17551757 const context = await this . _getContext ( )
1756- return findElement ( context , locator )
1758+ const elements = await findElements . call ( this , context , locator )
1759+ if ( elements . length === 0 ) {
1760+ throw new ElementNotFound ( locator , 'Element' , 'was not found' )
1761+ }
1762+ if ( this . options . strict ) assertOnlyOneElement ( elements , locator )
1763+ return elements [ 0 ]
17571764 }
17581765
17591766 /**
@@ -1768,6 +1775,7 @@ class Playwright extends Helper {
17681775 const context = providedContext || ( await this . _getContext ( ) )
17691776 const els = await findCheckable . call ( this , locator , context )
17701777 assertElementExists ( els [ 0 ] , locator , 'Checkbox or radio' )
1778+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
17711779 return els [ 0 ]
17721780 }
17731781
@@ -2240,6 +2248,7 @@ class Playwright extends Helper {
22402248 async fillField ( field , value ) {
22412249 const els = await findFields . call ( this , field )
22422250 assertElementExists ( els , field , 'Field' )
2251+ if ( this . options . strict ) assertOnlyOneElement ( els , field )
22432252 const el = els [ 0 ]
22442253
22452254 await el . clear ( )
@@ -2272,6 +2281,7 @@ class Playwright extends Helper {
22722281 async clearField ( locator , options = { } ) {
22732282 const els = await findFields . call ( this , locator )
22742283 assertElementExists ( els , locator , 'Field to clear' )
2284+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
22752285
22762286 const el = els [ 0 ]
22772287
@@ -2288,6 +2298,7 @@ class Playwright extends Helper {
22882298 async appendField ( field , value ) {
22892299 const els = await findFields . call ( this , field )
22902300 assertElementExists ( els , field , 'Field' )
2301+ if ( this . options . strict ) assertOnlyOneElement ( els , field )
22912302 await highlightActiveElement . call ( this , els [ 0 ] )
22922303 await els [ 0 ] . press ( 'End' )
22932304 await els [ 0 ] . type ( value . toString ( ) , { delay : this . options . pressKeyDelay } )
@@ -2330,23 +2341,30 @@ class Playwright extends Helper {
23302341 * {{> selectOption }}
23312342 */
23322343 async selectOption ( select , option ) {
2333- const els = await findFields . call ( this , select )
2334- assertElementExists ( els , select , 'Selectable field' )
2335- const el = els [ 0 ]
2336-
2337- await highlightActiveElement . call ( this , el )
2338- let optionToSelect = ''
2344+ const context = await this . context
2345+ const matchedLocator = new Locator ( select )
23392346
2340- try {
2341- optionToSelect = ( await el . locator ( 'option' , { hasText : option } ) . textContent ( ) ) . trim ( )
2342- } catch ( e ) {
2343- optionToSelect = option
2347+ // Strict locator
2348+ if ( ! matchedLocator . isFuzzy ( ) ) {
2349+ this . debugSection ( 'SelectOption' , `Strict: ${ JSON . stringify ( select ) } ` )
2350+ const els = await this . _locate ( matchedLocator )
2351+ assertElementExists ( els , select , 'Selectable element' )
2352+ return proceedSelect . call ( this , context , els [ 0 ] , option )
23442353 }
23452354
2346- if ( ! Array . isArray ( option ) ) option = [ optionToSelect ]
2355+ // Fuzzy: try combobox
2356+ this . debugSection ( 'SelectOption' , `Fuzzy: "${ matchedLocator . value } "` )
2357+ let els = await findByRole ( context , { role : 'combobox' , name : matchedLocator . value } )
2358+ if ( els ?. length ) return proceedSelect . call ( this , context , els [ 0 ] , option )
23472359
2348- await el . selectOption ( option )
2349- return this . _waitForAction ( )
2360+ // Fuzzy: try listbox
2361+ els = await findByRole ( context , { role : 'listbox' , name : matchedLocator . value } )
2362+ if ( els ?. length ) return proceedSelect . call ( this , context , els [ 0 ] , option )
2363+
2364+ // Fuzzy: try native select
2365+ els = await findFields . call ( this , select )
2366+ assertElementExists ( els , select , 'Selectable element' )
2367+ return proceedSelect . call ( this , context , els [ 0 ] , option )
23502368 }
23512369
23522370 /**
@@ -4102,6 +4120,14 @@ async function handleRoleLocator(context, locator) {
41024120 return context . getByRole ( locator . role , Object . keys ( options ) . length > 0 ? options : undefined ) . all ( )
41034121}
41044122
4123+ async function findByRole ( context , locator ) {
4124+ if ( ! locator || ! locator . role ) return null
4125+ const options = { }
4126+ if ( locator . name ) options . name = locator . name
4127+ if ( locator . exact !== undefined ) options . exact = locator . exact
4128+ return context . getByRole ( locator . role , Object . keys ( options ) . length > 0 ? options : undefined ) . all ( )
4129+ }
4130+
41054131async function findElements ( matcher , locator ) {
41064132 // Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
41074133 const isReactLocator = locator . type === 'react' || ( locator . locator && locator . locator . react ) || locator . react
@@ -4184,34 +4210,53 @@ async function proceedClick(locator, context = null, options = {}) {
41844210async function findClickable ( matcher , locator ) {
41854211 const matchedLocator = new Locator ( locator )
41864212
4187- if ( ! matchedLocator . isFuzzy ( ) ) return findElements . call ( this , matcher , matchedLocator )
4213+ if ( ! matchedLocator . isFuzzy ( ) ) {
4214+ const els = await findElements . call ( this , matcher , matchedLocator )
4215+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4216+ return els
4217+ }
41884218
41894219 let els
41904220 const literal = xpathLocator . literal ( matchedLocator . value )
41914221
41924222 try {
41934223 els = await matcher . getByRole ( 'button' , { name : matchedLocator . value } ) . all ( )
4194- if ( els . length ) return els
4224+ if ( els . length ) {
4225+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4226+ return els
4227+ }
41954228 } catch ( err ) {
41964229 // getByRole not supported or failed
41974230 }
41984231
41994232 try {
42004233 els = await matcher . getByRole ( 'link' , { name : matchedLocator . value } ) . all ( )
4201- if ( els . length ) return els
4234+ if ( els . length ) {
4235+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4236+ return els
4237+ }
42024238 } catch ( err ) {
42034239 // getByRole not supported or failed
42044240 }
42054241
42064242 els = await findElements . call ( this , matcher , Locator . clickable . narrow ( literal ) )
4207- if ( els . length ) return els
4243+ if ( els . length ) {
4244+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4245+ return els
4246+ }
42084247
42094248 els = await findElements . call ( this , matcher , Locator . clickable . wide ( literal ) )
4210- if ( els . length ) return els
4249+ if ( els . length ) {
4250+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4251+ return els
4252+ }
42114253
42124254 try {
42134255 els = await findElements . call ( this , matcher , Locator . clickable . self ( literal ) )
4214- if ( els . length ) return els
4256+ if ( els . length ) {
4257+ if ( this . options . strict ) assertOnlyOneElement ( els , locator )
4258+ return els
4259+ }
42154260 } catch ( err ) {
42164261 // Do nothing
42174262 }
@@ -4314,6 +4359,52 @@ async function findFields(locator) {
43144359 return this . _locate ( { css : locator } )
43154360}
43164361
4362+ async function proceedSelect ( context , el , option ) {
4363+ const role = await el . getAttribute ( 'role' )
4364+ const options = Array . isArray ( option ) ? option : [ option ]
4365+
4366+ if ( role === 'combobox' ) {
4367+ this . debugSection ( 'SelectOption' , 'Expanding combobox' )
4368+ await highlightActiveElement . call ( this , el )
4369+ const [ ariaOwns , ariaControls ] = await Promise . all ( [ el . getAttribute ( 'aria-owns' ) , el . getAttribute ( 'aria-controls' ) ] )
4370+ await el . click ( )
4371+ await this . _waitForAction ( )
4372+
4373+ const listboxId = ariaOwns || ariaControls
4374+ let listbox = listboxId ? context . locator ( `#${ listboxId } ` ) . first ( ) : null
4375+ if ( ! listbox || ! ( await listbox . count ( ) ) ) listbox = context . getByRole ( 'listbox' ) . first ( )
4376+
4377+ for ( const opt of options ) {
4378+ const optEl = listbox . getByRole ( 'option' , { name : opt } ) . first ( )
4379+ this . debugSection ( 'SelectOption' , `Clicking: "${ opt } "` )
4380+ await highlightActiveElement . call ( this , optEl )
4381+ await optEl . click ( )
4382+ }
4383+ return this . _waitForAction ( )
4384+ }
4385+
4386+ if ( role === 'listbox' ) {
4387+ for ( const opt of options ) {
4388+ const optEl = el . getByRole ( 'option' , { name : opt } ) . first ( )
4389+ this . debugSection ( 'SelectOption' , `Clicking: "${ opt } "` )
4390+ await highlightActiveElement . call ( this , optEl )
4391+ await optEl . click ( )
4392+ }
4393+ return this . _waitForAction ( )
4394+ }
4395+
4396+ await highlightActiveElement . call ( this , el )
4397+ let optionToSelect = option
4398+ try {
4399+ optionToSelect = ( await el . locator ( 'option' , { hasText : option } ) . textContent ( ) ) . trim ( )
4400+ } catch ( e ) {
4401+ optionToSelect = option
4402+ }
4403+ if ( ! Array . isArray ( option ) ) option = [ optionToSelect ]
4404+ await el . selectOption ( option )
4405+ return this . _waitForAction ( )
4406+ }
4407+
43174408async function proceedSeeInField ( assertType , field , value ) {
43184409 const els = await findFields . call ( this , field )
43194410 assertElementExists ( els , field , 'Field' )
@@ -4429,6 +4520,12 @@ function assertElementExists(res, locator, prefix, suffix) {
44294520 }
44304521}
44314522
4523+ function assertOnlyOneElement ( elements , locator ) {
4524+ if ( elements . length > 1 ) {
4525+ throw new MultipleElementsFound ( locator , elements )
4526+ }
4527+ }
4528+
44324529function $XPath ( element , selector ) {
44334530 const found = document . evaluate ( selector , element || document . body , null , 5 , null )
44344531 const res = [ ]
0 commit comments