Skip to content

refactor!: identify WebdriverIO objects by the wdio.kind brand - #2305

Merged
dprevost-LMI merged 11 commits into
webdriverio:mainfrom
dprevost-LMI:v8/wdio-kind
Oct 6, 2026
Merged

dprevost-LMI merged 11 commits into
webdriverio:mainfrom
dprevost-LMI:v8/wdio-kind

Conversation

@dprevost-LMI

@dprevost-LMI dprevost-LMI commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #2296

Proposed changes

WebdriverIO v10 brands its objects with Symbol.for('wdio.kind'): browser, browsing-context, element, element-array or mock. The matchers now identify objects by this brand, not by their shape ('getElements' in, parent, calls) or their constructor name.

  • src/util/wdioKind.ts: getWdioKind() reads the brand with Symbol.for(), without a dependency on @wdio/utils. getLoadedWdioKind() gives the brand only when the value has no then, the same rule as in @wdio/utils: a not-awaited $() is a Promise with the element brand, and a not-awaited $$() is a list with the element-array brand and a then.
  • isElement(), isStrictlyElementArray(), isMultiRemoteElement(), isMultiRemoteElementArray(), isBrowser(), isMultiRemoteBrowser(), isMock() and isMultiRemoteMock() keep their names and signatures, so their callers do not change.
  • isStrictlyElementArray() also checks Array.isArray(): a chained $('a').$$('b') or a custom $$ command before await is a Promise with the same brand.
  • A BrowsingContext has the constructor name Browser, so isBrowser() accepted it before. It still does, so the failure message does not change. [v8] feat: Support a WebdriverIO v10 BrowsingContext (tab, window, frame) in the browser matchers #2298 decides this (TODO(#2298)).
  • No v9 fallback: v8 requires WebdriverIO v10 (build!: require WebdriverIO v10 #2299).

Fix: false pass with a not-awaited multi-remote $$()

In v10, a not-awaited $$() is the list itself, not a Promise. Until the list is loaded, it has then, and its length is a Promise. The old instanceof Promise check did not await it, so a not-awaited multi-remote $$() counted 0 elements: toBeElementsArrayOfSize(0) passed on a page with elements, and the element matchers failed with wait: 0. awaitElementOrArray() and awaitElementArray() now always await the received value. await gives back a value that is not a thenable, and it gives the same list, so a refetch still updates the user's list in place. This bug was there before this PR. WebdriverIO docs issue: webdriverio/webdriverio#15935.

Unit test mocks

The mocks now match WebdriverIO 10.0.0. Each mock has its brand. A not-awaited $() is a Promise with the element brand. A not-awaited $$(), multi-remote or not, is a list that loads later and throws on for...of until it is loaded. The tests that expected the v9 shape (a $$() that is a Promise) now expect the v10 shape.

Breaking change

An object without the wdio.kind brand is not a browser, element, element list or mock. Real WebdriverIO v10 objects have the brand. A hand-made fake in a user's own unit tests needs it: see docs/Migrations.md, "WebdriverIO objects are identified by their brand". An array of unbranded fake mocks now throws a TypeError, as any other value that is not a mock.

How you tested

  • Unit tests (a new test fails before the fix)
    • pnpm run checks:all: 2574 passed, 40 skipped (the same skips as on main).
    • The tests for unbranded fakes, and for a browsing context with parent and getElement, failed before the brand change.
    • The not-awaited multi-remote $$() tests (toBeElementsArrayOfSize 2 and 0, toBeDisplayed with wait: 0) failed before the fix.
    • Guard tests fail without the line they protect: without Array.isArray() in isStrictlyElementArray(), the chained $().$$() test fails. If the failure message accepts a parent that is not loaded, the parent-walk test fails.
  • pnpm run test:types: jasmine 37, jasmine-global-expect-async 40, jest 61 and mocha 123 passed. ts:declarations and ts:package passed.
  • Playground run: pnpm run playgrounds:checks:all passed for the 5 playgrounds: browser-runner 1/1, jasmine 7/7, jest 39/39, mocha 7/7, and multi-remote 6/6 (Chrome + Firefox). The new multi-remote test "should count the elements of a not-awaited $$() at once, without a retry" failed with the build before the fix, and passes with it.
  • Before writing the code, I checked the brands of 45 objects in real Chrome and Firefox sessions with WebdriverIO 10.0.0, single and multi-remote.

🤖 Generated with Claude Code

dprevost-LMI and others added 11 commits October 5, 2026 22:31
WebdriverIO v10 brands its browsers, browsing contexts, elements,
element lists and mocks with `Symbol.for('wdio.kind')`, and a
not-awaited `$()` with `Symbol.for('wdio.chainable')`. `getWdioKind()`
and `isChainable()` read them without a dependency on `@wdio/utils`.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mocks of the browser, the elements, the element lists and the network
mocks now have the `wdio.kind` brand, and a not-awaited `$()` has
`wdio.chainable`, as in WebdriverIO 10.0.0.

The not-awaited `$$()` mock was a Proxy of a Promise, as in v9. In
v10 it is the element list itself: `Array.isArray()` is true,
`'getElements' in` is true, it has `then`, `catch` and `finally` until
it is awaited, its `length` is a Promise until then, and awaiting it
gives the same list. The tests that expected the v9 shape now expect
the v10 one, so the unit tests take the path that production takes.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`isElement()`, `isStrictlyElementArray()`, `isMultiRemoteElement()` and
`isMultiRemoteElementArray()` now read the WebdriverIO v10 `wdio.kind`
brand, not the shape of the object (`parent`, `foundWith`,
`getElement`, `getElements`, `selector`). A frame browsing context also
has a `parent`, and a fake with the same properties is not a WebdriverIO
object. A not-awaited `$()` has the `element` brand, but it is a Promise,
so `isElement()` also checks `wdio.chainable`.

The failure message walks the parents of an element while they are
awaited elements or lists, by their brand.

An unbranded copy of a list (`[...elements]`, `elements.map()`) is still
an `Element[]`, because its items have the brand.

BREAKING CHANGE: an object without the WebdriverIO v10 `wdio.kind` brand
is not an element or an element list.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`isBrowser()` read the constructor name (`Browser`, `MultiRemoteDriver`),
an internal name of `@wdio/utils`. It now reads the WebdriverIO v10
`wdio.kind` brand: `browser`, or `browsing-context`, which also has the
`Browser` constructor name. So the failure message of a browsing
context does not change (TODO(webdriverio#2298)).

`isMultiRemoteBrowser()` also checks the `browser` brand: a multi-remote
element has the multi-remote flag too. `isMock()` and
`isMultiRemoteMock()` read the `mock` brand, not the `calls` array or
the `instances` and `getInstance` of the object.

BREAKING CHANGE: an object without the WebdriverIO v10 `wdio.kind` brand
is not a browser or a mock. An array of such fake mocks is not checked
per instance anymore.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The matchers identify WebdriverIO objects by the WebdriverIO v10
`wdio.kind` brand, so a hand-made fake needs it.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In WebdriverIO v10, a not-awaited `$$()` is the element list itself, not
a Promise, and its `length` is a Promise until the list is loaded. A
not-awaited multi-remote `$$()` was used as is, so it counted 0
elements: `toBeElementsArrayOfSize(0)` passed on a page with elements,
and the element matchers failed with `wait: 0`.

`awaitElementOrArray()` and `awaitElementArray()` now also await an
element list that still has `then` (`isNotAwaitedElementList()`).
Awaiting it gives the same list, so a refetch still updates the user's
list in place.

The list mocks load later, like the `load()` of WebdriverIO v10, and the
multi-remote `$$()` mock is a not-awaited list, not a Promise.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The matchers call `getElement()` on an element, so the example threw a
TypeError.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…driverIO 10.0.0

`multiRemoteBrowser.$()` and the `$()` of a multi-remote element are now
Promises with the `element` brand and `wdio.chainable`, and the `$$()`
of a multi-remote element is a list that is not loaded yet. Before
they are loaded, the list mocks now throw on `for...of` and spread, as
WebdriverIO 10.0.0 does.

New tests cover these not-awaited multi-remote values in `toBeDisplayed`
and `toBeElementsArrayOfSize`. Without the await of a not-loaded list,
the `$$()` tests fail.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d element

In WebdriverIO v10, a chained `$('a').$$('b')` or a custom `$$` command
before `await` is a Promise proxy with the `element-array` brand. Only
`Array.isArray()` keeps `isStrictlyElementArray()` from accepting it, and
no test failed when it was removed: a new test now does.

The comment of `isElement()` now says that an item of an awaited `$$()`
and the result of `getElement()` are elements too.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`awaitElementOrArray()` and `awaitElementArray()` checked for a Promise or
for a list with the `element-array` brand and a `then` before they awaited
the received value. A plain `await` is simpler and does the same: it gives
back a value that is not a thenable, and it awaits each pending value,
a not-awaited `$$()` included. `isNotAwaitedElementList()` is not used
anymore, so it is removed.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`isElement()`, `isMultiRemoteElement()` and the parent walk of the
failure message excluded a not-awaited element with the
`wdio.chainable` flag. In WebdriverIO 10.0.0, each element with that
flag is a Promise, and a loaded element has no `then`. So
`getLoadedWdioKind()`, the same rule as `getLoadedWdioKind()` of
`@wdio/utils`, gives the brand of a value only when it has no `then`.
The same rule as the plain `await` now applies everywhere, and
`isChainable()` and `WDIO_CHAINABLE` are removed from `src/`. The mocks
still set the flag, as WebdriverIO 10.0.0 does.

A new test checks that the failure message stops its walk at a parent
that is not loaded (a chained `$('form').$('input')` before `await`).
It fails if the walk accepts that parent.

Refs webdriverio#2296

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dprevost-LMI
dprevost-LMI marked this pull request as ready for review October 6, 2026 10:59
@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Refactors object type detection to use WebdriverIO v10 brand symbols.

The PR appears safe to merge; no actionable defect was found.

What we checked:

  • Only fixed Promise methods run: Both branches allow only catch and finally on the Promise returned by settle.
  • Lists load before counting: awaitElementArray awaits the received value before returning it. toBeElementsArrayOfSize waits for that result before reading the length or counting each browser's elements.

Summary

The PR identifies WebdriverIO objects through Symbol.for('wdio.kind') instead of their properties or constructor names.

  • Always awaits received element lists before matchers use them, including multi-remote $$() results.
  • Updates mocks and adds tests for unloaded lists, branded objects, and selector messages.
  • Documents the required brand for hand-made test objects.
  • No actionable issues were found.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Received element or list"] --> B["Await received value"]
    B --> C{"Read wdio.kind"}
    C --> D["Single element: getElement"]
    C --> E["Single-browser list: getElements"]
    C --> F["Multi-remote element or list"]
    C --> G["Unrecognized value"]
    D --> H["Compare values"]
    E --> H
    F --> H
    H --> I["Refetch eligible lists on retry"]
Loading

Reviews (1) · Last reviewed commit: "refactor: find a loaded element by its b..."

@dprevost-LMI dprevost-LMI changed the title V8/wdio kind refactor!: identify WebdriverIO objects by the wdio.kind brand Oct 6, 2026
@dprevost-LMI
dprevost-LMI merged commit 49633d7 into webdriverio:main Oct 6, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v8] Identify WebdriverIO v10 objects with the wdio.kind brand in place of shape and class-name checks

1 participant