Debugging debugging devtools chrome firefox

Testing XPath in DevTools

Verify every locator in the browser before it goes into a test: the Elements search box, $x() in the console, document.evaluate, and the Copy XPath trap.

Testing XPath in DevTools

Never put an XPath into a test without checking it in the browser first. It takes five seconds and saves a failed pipeline run. Chrome, Edge and Firefox all give you two tools for this.

  1. Open DevTools (F12, or Ctrl+Shift+I / Cmd+Option+I).
  2. Go to the Elements tab (Firefox: Inspector).
  3. Press Ctrl+F (Cmd+F on Mac) to open the search bar at the bottom.
  4. Paste your XPath and press Enter.

The bar shows 1 of 3, the matches are highlighted in the tree, and Enter cycles through them. It understands XPath, CSS selectors and plain text, and decides which based on the input, so anything starting with / or ( is treated as XPath.

This is the fastest way to check how many elements a locator matches. A locator you expect to be unique should show 1 of 1.

Tool 2: $x() in the Console

The console has a helper that evaluates XPath and returns an array of nodes:

$x("//button[@type='submit']")          // array of elements
$x("//button[@type='submit']").length   // count
$x("//button[@type='submit']")[0]       // first match; hover it to highlight on the page
$x("//h1/text()")                       // text nodes
$x("//a/@href").map(a => a.value)       // attribute values
$x("count(//tr)")                       // numbers work too in Chrome

$x is a console-only convenience; it does not exist in page scripts or in your test code. Right-click a result and choose Reveal in Elements panel to jump to it.

Optional second argument: a context node. $x("./td[2]", row) evaluates relative to row.

Tool 3: document.evaluate

The real API, which works anywhere JavaScript runs in the page (including Selenium’s executeScript and Playwright’s page.evaluate):

const result = document.evaluate(
  "//button[normalize-space()='Save']",
  document,
  null,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  null
);
for (let i = 0; i < result.snapshotLength; i++) {
  console.log(result.snapshotItem(i));
}

Use XPathResult.FIRST_ORDERED_NODE_TYPE and .singleNodeValue for one element, NUMBER_TYPE and .numberValue for count(), STRING_TYPE and .stringValue for string().

Finding the element to start from

  1. Right-click the element on the page, choose Inspect. The Elements panel jumps to it.
  2. Read the attributes: is there a data-testid, name, aria-label, id, or meaningful text?
  3. Look up the tree for a container: form, [role=dialog], table, a component with a test id.
  4. Write the locator from that container down.
  5. Test it with Ctrl+F. Adjust until it shows 1 of 1.

The Copy XPath trap

Right-click an element in the Elements panel and there is Copy > Copy XPath and Copy full XPath. They produce:

//*[@id="mat-input-3"]                          Copy XPath (id-anchored)
/html/body/div[2]/div/main/form/div[3]/input    Copy full XPath (absolute)

The first depends on an id that may be generated. The second breaks on any layout change. Use them to confirm which element you are looking at, then write your own locator. See Dynamic IDs and Generated Classes.

Checking what the text really is

If a text match fails, inspect the text node itself:

$x("//button[contains(., 'Sign')]")[0].textContent
// "\n      Sign In\n    "
[...$x("//span[@class='total']")[0].textContent].map(c => c.charCodeAt(0))
// reveals non-breaking spaces (160) and zero-width characters

Checking frames and shadow roots

If $x finds nothing but the element is clearly there:

  • Look for #shadow-root above it in the Elements tree. If present, XPath cannot reach it from the document. See Shadow DOM and iframes.
  • Look for <iframe> above it. Switch the console’s context using the frame dropdown at the top of the Console panel (it says top by default), then run $x again.

Firefox differences

Firefox’s Inspector search also accepts XPath, and its console has $x() too. The Copy XPath option is under Copy in the context menu. Behaviour is the same otherwise, since both browsers implement the same DOM XPath API.

Try It Yourself

Open this site’s own playground, press F12, and run in the console:

$x("//iframe")[0].contentDocument.evaluate("//button", $x("//iframe")[0].contentDocument, null, 7, null).snapshotLength

That evaluates XPath inside the playground’s preview frame, which is what the playground itself does.

Next Steps

  1. Common Errors and Fixes - Decoding the error messages
  2. Performance and Efficiency - When a slow locator is your fault
  3. XPath Tools - Validator, robustness checker and code generator