[🐛 Bug]: Stale BiDi shadow root cache produces "SharedId belongs to different document" warnings after page-initiated navigations
#15.467 geöffnet am 05.08.2026
Repository-Metriken
- Stars
- (6.029 Sterne)
- PR-Merge-Metriken
- (Durchschn. Merge 15T 15h) (48 gemergte PRs in 30 T)
Beschreibung
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:
#handleBidiCommandclears the cache when it sees abrowsingContext.navigatecommand sent through the protocol. Page-initiated navigations (e.g. a button click that setslocation.hrefand redirects to an IdP, then a redirect back) never send such a command.handleLogEntrypurges the cache heuristically when the new document registers a custom element with a shadow root (newShadowRootlog + 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