webdriverio/webdriverio

[🐛 Bug]: Stale BiDi shadow root cache produces "SharedId belongs to different document" warnings after page-initiated navigations

Open

#15,467 opened on Aug 5, 2026

 (3 comments) (0 reactions) (0 assignees)JavaScript (1,793 forks)batch import
Bug 🐛Protocol Relatedgood first pickhelp wanted

Repository metrics

Stars
 (6,029 stars)
PR merge metrics
 (Avg merge 15d 15h) (48 merged PRs in 30d)

Description

WebdriverIO Version

9.30.1

Node.js Version

v24.18.0

Mode

WDIO Testrunner

Which capabilities are you using?

{
    browserName: 'chrome',
    browserVersion: 'stable', // Chrome 151.0.7922.72
    'goog:chromeOptions': {
        args: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
    }
}

ChromeDriver 151.0.7922.76, macOS (headless and headed).

What happened?

The ShadowRootManager (packages/webdriverio/src/session/shadowRoot.ts) caches the shadow roots of the current document per browsing context and passes them as startNodes to browsingContext.locateNodes on every element lookup (see findDeepElement in packages/webdriverio/src/commands/browser/$.ts). After a navigation, those cached SharedIds reference the old document, so the driver rejects them:

WARN webdriverio: Failed to execute browser.browsingContextLocateNodes({ ... }) due to Error: WebDriver Bidi command "browsingContext.locateNodes" failed with error: no such node - SharedId "f.….d.<old-document-id>.e.…" belongs to different document. Current document is <new-document-id>., falling back to regular WebDriver Classic command

WDIO only invalidates that cache in two situations, both of which miss common navigation flows:

  1. #handleBidiCommand clears the cache when it sees a browsingContext.navigate command sent through the protocol. Page-initiated navigations (e.g. a button click that sets location.href and redirects to an IdP, then a redirect back) never send such a command.
  2. handleLogEntry purges the cache heuristically when the new document registers a custom element with a shadow root (newShadowRoot log + document id change). If the destination page uses light DOM (no custom elements — e.g. most IdP login pages), that purge never fires.

Additionally, the manager subscribes to browsingContext.navigationStarted in its constructor but never registers a handler for it, so that event is effectively unused.

The warning is benign (WDIO falls back to the classic WebDriver command and still finds the element), but it produces a flood of WARN log noise after every page-initiated navigation in a shadow-DOM-heavy application, and it only gets worse the more shadow roots are cached.

What is your expected behavior?

The shadow root cache should be invalidated whenever a navigation starts, regardless of whether it was driver-initiated or page-initiated. In particular, the browsingContext.navigationStarted event that the manager already subscribes to should clear the cache for the navigating context.

How to reproduce the bug.

Serve index.html locally, then run the WDIO spec below.

index.html (shadow-DOM page that redirects via a plain link):

<!doctype html>
<html>
<body>
    <custom-app></custom-app>
    <a id="go" href="https://idp.example/login">Go</a>
    <script>
        customElements.define('custom-app', class extends HTMLElement {
            constructor() { super().attachShadow({ mode: 'open' }).innerHTML = '<p>Hi</p>' }
        })
    </script>
</body>
</html>

https://idp.example/login is a plain light-DOM page (no custom elements).

test.js:

const { browser, expect } = require('@wdio/globals')

describe('shadow root cache after page-initiated navigation', () => {
    it('should not warn on a light-DOM page after a page-initiated navigation', async () => {
        await browser.url('http://localhost:8080/')
        await expect($('custom-app')).toBeExisting() // caches the shadow root
        await $('#go').click()                       // page-initiated navigation to idp.example
        await browser.pause(1000)
        await expect($('#username')).toBeExisting()  // triggers the locateNodes fallback warning
    })
})

The last lookup logs the SharedId ... belongs to different document WARN (and every subsequent element lookup while on the IdP page does too).

Environment: Chrome 151.0.7922.72 + ChromeDriver 151.0.7922.76 on macOS.

Relevant log output

[0-0] WARN webdriverio: Failed to execute browser.browsingContextLocateNodes({ ... }) due to Error: WebDriver Bidi command "browsingContext.locateNodes" failed with error: no such node - SharedId "f.D8BDE8237F7EBFB34F17D436A0053A5F.d.669DB3AE919B97D30BCF1E8C53E89E5D.e.230" belongs to different document. Current document is C2FD99F91AC3080D3479CAF1827E58C0., falling back to regular WebDriver Classic command

patch-package fix

I worked around it via patch-package by handling the already-subscribed browsingContext.navigationStarted event and clearing the cache for the navigating context:

   #handleLogEntryListener = this.handleLogEntry.bind(this);
   #commandResultHandlerListener = this.#commandResultHandler.bind(this);
   #handleBidiCommandListener = this.#handleBidiCommand.bind(this);
+  #handleNavigationStartedListener = this.#handleNavigationStarted.bind(this);
   constructor(browser) {
     ...
     this.#browser.on("log.entryAdded", this.#handleLogEntryListener);
     this.#browser.on("result", this.#commandResultHandlerListener);
     this.#browser.on("bidiCommand", this.#handleBidiCommandListener);
+    this.#browser.on("browsingContext.navigationStarted", this.#handleNavigationStartedListener);
     ...
   }
   removeListeners() {
     super.removeListeners();
     this.#browser.off("log.entryAdded", this.#handleLogEntryListener);
     this.#browser.off("result", this.#commandResultHandlerListener);
     this.#browser.off("bidiCommand", this.#handleBidiCommandListener);
+    this.#browser.off("browsingContext.navigationStarted", this.#handleNavigationStartedListener);
   }
+  /**
+   * clear cached shadow roots as soon as a navigation starts (including
+   * page-initiated ones) so stale sharedIds are not passed to locateNodes
+   */
+  #handleNavigationStarted(nav) {
+    this.#shadowRoots.delete(nav.context);
+    this.#currentDocumentIds.delete(nav.context);
+  }

Apply with:

npx patch-package webdriverio

Contributor guide