Skip to content

Commit b11144f

Browse files
DavertMikclaude
andcommitted
feat(CDPBrowser): support elementIndex/strict-mode step options
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e84835c commit b11144f

3 files changed

Lines changed: 57 additions & 7 deletions

File tree

lib/helper/CDPBrowser.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Locator from '../locator.js'
99
import store from '../store.js'
1010
import { xpathLocator, normalizePath, resolveUrl, toCamelCase, convertCssPropertiesToCamelCase, normalizeSpacesInString } from '../utils.js'
1111
import ElementNotFound from './errors/ElementNotFound.js'
12+
import MultipleElementsFound from './errors/MultipleElementsFound.js'
1213
import { includes as stringIncludes } from '../assert/include.js'
1314
import { empty } from '../assert/empty.js'
1415
import { 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
/**

lib/helper/clientscripts/cdpBrowserClient.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,23 @@ export default function installCodeceptClient() {
166166
}).length,
167167
}
168168
window.__codecept = {
169-
run(candidates, action, payload, within) {
169+
run(candidates, action, payload, within, selection) {
170170
let root
171171
if (within) {
172172
const withinEls = find(within, document)
173173
if (!withinEls.length) return { found: 0, withinMissing: true }
174174
root = withinEls[0]
175175
}
176-
const els = candidates === null ? [root] : find(candidates, root)
176+
let els = candidates === null ? [root] : find(candidates, root)
177+
if (selection && els.length > 1) {
178+
if (selection.index != null) {
179+
const idx = selection.index > 0 ? selection.index - 1 : els.length + selection.index
180+
if (idx < 0 || idx >= els.length) return { found: els.length, outOfBounds: true, requestedIndex: selection.index }
181+
els = [els[idx]]
182+
} else if (selection.strict) {
183+
return { found: els.length, strictViolation: true }
184+
}
185+
}
177186
if (!els.length && action !== 'count' && action !== 'visibleCount') return { found: 0 }
178187
return { found: els.length, result: actions[action](els, payload || {}) }
179188
},

test/helper/webapi.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2613,9 +2613,6 @@ export function tests() {
26132613
})
26142614

26152615
describe('#elementIndex step option', () => {
2616-
beforeEach(function () {
2617-
if (isHelper('CDPBrowser')) this.skip() // store.currentStep.opts.elementIndex is not implemented in CDPBrowser
2618-
})
26192616
afterEach(() => {
26202617
store.currentStep = null
26212618
I.options.strict = false

0 commit comments

Comments
 (0)