@@ -9,6 +9,7 @@ import Locator from '../locator.js'
99import store from '../store.js'
1010import { xpathLocator , normalizePath , resolveUrl , toCamelCase , convertCssPropertiesToCamelCase , normalizeSpacesInString } from '../utils.js'
1111import ElementNotFound from './errors/ElementNotFound.js'
12+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
1213import { includes as stringIncludes } from '../assert/include.js'
1314import { empty } from '../assert/empty.js'
1415import { truth } from '../assert/truth.js'
@@ -260,9 +261,52 @@ class CDPBrowser extends Helper {
260261 */
261262 async _run ( candidates , action , payload ) {
262263 await this . _ensureClient ( )
263- return this . _evaluate (
264- `window.__codecept.run(${ JSON . stringify ( candidates ) } , ${ JSON . stringify ( action ) } , ${ JSON . stringify ( payload || null ) } , ${ JSON . stringify ( this . withinCandidates ) } )` ,
264+ const selection = this . _selectionDescriptor ( )
265+ const res = await this . _evaluate (
266+ `window.__codecept.run(${ JSON . stringify ( candidates ) } , ${ JSON . stringify ( action ) } , ${ JSON . stringify ( payload || null ) } , ${ JSON . stringify ( this . withinCandidates ) } , ${ JSON . stringify ( selection ) } )` ,
265267 )
268+ if ( res ?. outOfBounds ) {
269+ throw new Error ( `elementIndex ${ res . requestedIndex } exceeds the number of elements found (${ res . found } ) for "${ this . _candidatesLabel ( candidates ) } "` )
270+ }
271+ if ( res ?. strictViolation ) {
272+ throw new MultipleElementsFound ( this . _candidatesLabel ( candidates ) , new Array ( res . found ) )
273+ }
274+ return res
275+ }
276+
277+ /**
278+ * Builds the `{index, strict}` element-selection descriptor from the current step's options
279+ * (`store.currentStep.opts`) and `options.strict`, mirroring the semantics of
280+ * `lib/helper/extras/elementSelection.js` (used by Puppeteer/WebDriver): a per-step
281+ * `elementIndex` (numeric, or the `'first'`/`'last'` aliases) always takes precedence and
282+ * disables strict mode for that step; otherwise `exact`/`strictMode` per-step options
283+ * override `options.strict` to enable or cancel strict mode.
284+ *
285+ * @returns {?{index?: number, strict?: true} } `null` when neither applies.
286+ * @protected
287+ */
288+ _selectionDescriptor ( ) {
289+ const opts = store . currentStep ?. opts
290+ let index = opts ?. elementIndex
291+ if ( index === 'first' ) index = 1
292+ else if ( index === 'last' ) index = - 1
293+ if ( index !== undefined && index !== null ) return { index }
294+ let strict = ! ! this . options . strict
295+ if ( opts ?. exact === true || opts ?. strictMode === true ) strict = true
296+ else if ( opts ?. exact === false || opts ?. strictMode === false ) strict = false
297+ return strict ? { strict : true } : null
298+ }
299+
300+ /**
301+ * A short, human-readable label built from `candidates`, used in `_run`'s elementIndex/strict
302+ * error messages when no locator string is otherwise available.
303+ *
304+ * @param {?Array<{type: 'css'|'xpath', value: string}> } candidates
305+ * @returns {string }
306+ * @protected
307+ */
308+ _candidatesLabel ( candidates ) {
309+ return ( candidates || [ ] ) . map ( c => c . value ) . join ( ' | ' )
266310 }
267311
268312 /**
0 commit comments