[🐛 Bug]: An index past the end of a $$([a, b]) list with mixed selectors throws a selector error and leaves an unhandled rejection · Issue #16122 · webdriverio/webdriverio · GitHub
Skip to content

[🐛 Bug]: An index past the end of a $$([a, b]) list with mixed selectors throws a selector error and leaves an unhandled rejection #16122

Open

Description

@plum117

Have you read the Contributing Guidelines on issues?

WebdriverIO Version

10.0.2 and main (ea05e5599)

Node.js Version

24.20.0

Mode

Standalone Mode

Which capabilities are you using?

{ browserName: 'chrome', 'goog:chromeOptions': { args: ['--headless=new'] } } // WebDriver BiDi

What happened?

An index past the end of a $$([a, b]) list whose elements come from different selectors fails with an error about the selector, not "not found":

// <ul><li>a</li><li>b</li></ul><p>c</p>, waitforTimeout: 500
const list = await browser.$$([await browser.$('li:first-child'), await browser.$('p')])
await list[5].isExisting()
// throws after 500 ms: waitUntil condition failed with the following reason:
// selector needs to be typeof `string` or `function`, but found: `undefined`

Reading the index without await (if (list[5])) gives an unhandled rejection with the same message after waitforTimeout.

List await list[5].isExisting() if (list[5]), no await
$$('li') (main) false after waitforTimeout no unhandled rejection
$$([li, p]), mixed selectors (10.0.2 and main) throws the selector error unhandled rejection

What is your expected behavior?

The same as for a $$(selector) list: isExisting() gives false, a command such as getText() throws "Index out of bounds", and reading the index does not leave an unhandled rejection. A list that cannot be queried again (no common selector) could also give undefined for an index past the end, as a slice() or filter() list does.

How to reproduce the bug.

The code above, in a standalone session with headless Chrome.

Relevant log output

waitUntil condition failed with the following reason: selector needs to be typeof `string` or `function`, but found: `undefined`

Code of Conduct

  • I agree to follow this project's Code of Conduct

Is there an existing issue for this?

  • I have searched the existing issues

Activity

  1. self-assigned this
    on Oct 17, 2026
  2. added
    help wantedIssues that are free to take by anyone interested
    good first picka reasonable task to start getting familiar with the code base
    on Oct 17, 2026
  3. wdio-bot commented on Oct 17, 2026

    @wdio-bot
    Contributor

    Thanks for reporting!

    We greatly appreciate any contributions that help resolve the bug. While we understand that active contributors have their own priorities, we kindly request your assistance if you rely on this bug being fixed. We encourage you to take a look at our contribution guidelines or join our friendly Discord development server, where you can ask any questions you may have. Thank you for your support, and cheers!

  4. wdio-bot commented on Oct 17, 2026

    @wdio-bot
    Contributor

    Triage summary

    Findings

    1. Why the selector error: $$([elemA, elemB]) normalizes its selector via normalizeSelector (packages/webdriverio/src/element/array.ts:74-94): when the elements come from different selectors the list has no selector (undefined). But for an index past the end, elementAt (array.ts:197-) still treats the list as an original query and calls parent[foundWith](selector, ...props) with selector === undefined. findElements then throws selector needs to be typeof `string` or `function`, but found: `undefined` (utils/index.ts:1347) inside browser.waitUntil, which wraps it as "waitUntil condition failed with the following reason: …". The catch at array.ts:229-232 rethrows it because the message isn't the timeoutMsg — so the "not found" branch (array.ts:243-268, via itemOf in utils/findElementAgain.ts:67-79, which correctly returns undefined for a selector-less list) is never reached. That's why you get the selector error instead of isExisting() === false / "Index out of bounds".

    2. Why the unhandled rejection: for a resolved list, readIndex (array.ts:181-195) returns chainElementPromise(elementAt(...)) — a truthy proxy. if (list[5]) sees the truthy proxy while the underlying elementAt promise rejects with no handler attached (handlers are only bound when then/catch/finally are accessed, wdio-utils/src/shim.ts:65,443) → unhandled rejection.

    3. The code comments already acknowledge this gap ("a list of elements or element references … still throws", array.ts:82-84 / findElementAgain.ts:64-67), so this is a real, localized bug rather than intended behavior — the escape hatch exists for unregistered custom strategies, but a $$([...]) element list falls into it by accident and surfaces an internal-looking error.

    Suggested fix direction

    A list whose elements have no common selector can never be re-queried, so it should behave like a derived (slice/filter) list: either

    • mark it non-refetchable when normalizeSelector yields undefined for an element-array input (mirroring derivedMetadata's refetch: false, array.ts:116-121), or
    • short-circuit in elementAt when selector === undefined before entering waitUntil — both make an out-of-range index resolve to undefined (your second expected-behavior option), which also removes the unhandled rejection.

    A regression test fits naturally into the existing "not found" describe block in packages/webdriverio/tests/element/array.test.ts:279+.

    Actions taken

    • Labels: kept Bug 🐛, added help wanted + good first pick
    • State: left open

    Next steps

    @plum117 — excellent report as always; the repro matrix and the two failure shapes made this quick to pin down. Want to raise a PR for this? The change is localized to packages/webdriverio/src/element/array.ts (+ a unit test), and your expected-behavior table already defines the acceptance criteria. Fork → pnpm install → pnpm run setup → focused tests, per CONTRIBUTING.md.

    This looks like a good candidate for an expensed contribution. If you open a PR, a maintainer can apply an Expensable $… label and run the expense workflow after merge.


    Automated triage assistant — maintainers may revise.

  5. removed
    help wantedIssues that are free to take by anyone interested
    good first picka reasonable task to start getting familiar with the code base
    on Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions