Skip to content

Instantly share code, notes, and snippets.

@jonathantneal
Last active January 25, 2026 08:42
Show Gist options
  • Select an option

  • Save jonathantneal/1b25514f6120254b152d1232896a9f1e to your computer and use it in GitHub Desktop.

Select an option

Save jonathantneal/1b25514f6120254b152d1232896a9f1e to your computer and use it in GitHub Desktop.
Logic used to get a NodeID from an element using CDP
const getNodeId = async (element: Element) => {
const selectors = getUniqueSelectors(element)
const root = (await cdp().send("DOM.getDocument", { depth: -1 })).root
let nodeId = root.nodeId
for (const selector of selectors) {
if (nodeId !== root.nodeId) {
const next = await cdp().send("DOM.describeNode", { nodeId })
if (next.node.contentDocument?.nodeId) {
nodeId = next.node.contentDocument.nodeId
}
}
nodeId = (
await cdp().send("DOM.querySelector", {
nodeId,
selector,
})
).nodeId
}
return nodeId
}
const getUniqueSelectors = (element: Element): string[] => {
const selector = getUniqueSelector(element)
const root = element.getRootNode() as Document
return root instanceof Document && root.defaultView?.frameElement
? [...getUniqueSelectors(root.defaultView.frameElement), selector]
: [selector]
}
const getUniqueSelector = (element: Element) => {
/** Unique selector for this element */
let selector = ""
let parent: Element
let nth: number
while ((parent = element.parentElement!)) {
for (nth = 1; (element = element.previousElementSibling!); ++nth);
selector = " > :nth-child(" + nth + ")" + selector
element = parent
}
return ":root" + selector
}
@ibnlanre
Copy link

This is my thanks to Jonathan, for showing us that it's possible.

import type { CDPSession, Locator } from "playwright";

import { randomBytes } from "node:crypto";

/**
 * Get CDP backendNodeId for a Playwright Locator.
 */
export async function getBackendNodeId(
  locator: Locator,
  cdp: CDPSession,
): Promise<number> {
  const nodeId = await getNodeId(locator, cdp);
  const { node } = await cdp.send("DOM.describeNode", { nodeId });
  return node.backendNodeId;
}

/**
 * Get CDP nodeId for a Playwright Locator.
 */
export async function getNodeId(
  locator: Locator,
  cdp: CDPSession,
): Promise<number> {
  const attribute = "data-cdp-node-id";
  const id = randomBytes(8).toString("hex");
  const selector = `[${attribute}="${id}"]`;

  try {
    await locator.evaluate(
      (el, { attribute, id }) => el.setAttribute(attribute, id),
      { attribute, id },
    );

    await cdp.send("DOM.enable");

    const { root } = await cdp.send("DOM.getDocument", { depth: 0 });
    const { nodeId } = await cdp.send("DOM.querySelector", {
      nodeId: root.nodeId,
      selector,
    });

    if (nodeId === 0) {
      throw new Error(`Element not found with selector: ${selector}`);
    }

    return nodeId;
  } finally {
    await locator
      .evaluate((el, attr) => el.removeAttribute(attr), attribute)
      .catch(() => {});
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment