XPath Interview Questions

49 questions the way they are actually asked, with answers you can defend. Each level builds on the one before. The scenario section is what a good interviewer will do to you on a whiteboard.

How to use this page

  • Read the question, answer it out loud, then open the model answer.
  • For scenario questions, write the expression in the playground before you check.
  • Interviewers care more about the reasoning (why this anchor, why not an index) than about the exact string.

Junior / Entry

Syntax, terminology and the basics every automation engineer is expected to know.

1 What is XPath and why is it used in test automation?

XPath is a query language for selecting nodes in XML and HTML documents. Test frameworks use it to locate elements because it can match on attributes, text and structure, and can navigate in every direction including upward, which CSS selectors cannot do.

basics
2 What is the difference between absolute and relative XPath? Which should you use?

Absolute XPath starts at the root with a single slash and lists every step: /html/body/div/form/input. Relative XPath starts with // and matches anywhere: //input[@name="email"]. Relative XPath should be used almost always, because absolute paths break whenever any ancestor changes.

/html/body/div[2]/form/input[1] Try →
//input[@name='email'] Try →
basicsrobustness
3 What does the @ symbol mean?

It selects an attribute. @id is the id attribute, @* is any attribute. //input[@type="email"] selects inputs whose type attribute equals email; //input[@required] selects inputs that have a required attribute at all.

syntax
4 What is a predicate?

The filter in square brackets. It is evaluated for each node the step selects, with that node as context, and keeps the node if the result is true. Predicates can be comparisons, existence tests, or numbers, which are shorthand for position()=n.

//button[@type='submit'] Try →
//li[3] Try →
//tr[td[normalize-space()='Total']] Try →
syntaxpredicates
5 How do you select an element by its text?

Use normalize-space() for an exact visible match: //button[normalize-space()="Save"]. contains(., "Sav") for a partial match. text()="Save" also works but fails on surrounding whitespace or nested elements, so it is less reliable.

//button[normalize-space()='Save'] Try →
//a[contains(., 'Sign')] Try →
text
6 What is the difference between / and // in an XPath?

/ steps to direct children; // steps to descendants at any depth. //ul/li selects li children of a ul; //form//input selects inputs anywhere inside a form.

syntax
7 How do you select all elements with a given class?

//*[contains(@class, "btn")] matches any element whose class attribute contains the substring. For an exact token, so btn does not match btn-danger, use contains(concat(" ", normalize-space(@class), " "), " btn ").

//*[contains(@class, 'btn')] Try →
//*[contains(concat(' ', normalize-space(@class), ' '), ' btn ')] Try →
attributes
8 What does //div[1] select?

Every div that is the first div child of its parent, not the first div in the document. To get the first div in the document, wrap it: (//div)[1]. This is one of the most common interview traps and one of the most common real bugs.

//div[1] Try →
(//div)[1] Try →
position
9 How do you test an XPath in the browser?

In DevTools, open the Elements panel, press Ctrl+F and paste the XPath; matches are highlighted with a count. Or in the Console, run $x("//your/xpath") to get an array of matching nodes.

debugging
10 Name three XPath functions and what they do.

contains(s, sub) tests for a substring; starts-with(s, prefix) tests the start; normalize-space(s) trims and collapses whitespace; text() selects text nodes; count(nodes) returns how many; last() returns the size of the current context; position() returns the index.

functions
11 What is the difference between findElement and findElements in Selenium when using XPath?

findElement returns the first matching WebElement or throws NoSuchElementException. findElements returns a list, empty if nothing matches, which makes it the right choice for asserting that something is absent.

selenium
12 How do you select an element that has a specific attribute, regardless of its value?

Use the attribute name alone as an existence test: //input[@disabled], //*[@data-testid]. This is true when the attribute is present with any value, including an empty string.

attributes

Mid-level

Axes, functions, text handling and the reasoning behind robust locators.

1 Explain the difference between text() and . in a predicate.

text() selects the direct text node children of the element; . is the element itself, whose string value is all descendant text concatenated. For <button><span>Save</span></button>, contains(text(), "Save") is false because the button has no text node of its own; contains(., "Save") is true. Also, when text() returns several nodes, string functions use only the first, whereas = tests all of them.

//button[contains(text(), 'Save')] Try →
//button[contains(., 'Save')] Try →
//button[text()[contains(., 'Save')]] Try →
text
2 List the XPath axes and give a use case for following-sibling and ancestor.

ancestor, ancestor-or-self, attribute, child, descendant, descendant-or-self, following, following-sibling, namespace, parent, preceding, preceding-sibling, self. following-sibling: the input after a label, //label[...]/following-sibling::input[1]. ancestor: the row containing a cell, //td[...]/ancestor::tr[1].

axes
3 Why does ancestor::div[1] give the nearest div rather than the outermost?

ancestor is a reverse axis. On reverse axes (ancestor, ancestor-or-self, preceding, preceding-sibling) positions count away from the context node, so [1] is the closest. Wrapping in parentheses, (//x/ancestor::div)[1], resets to document order and returns the outermost.

axesposition
4 How do you write a case-insensitive match in XPath 1.0?

There is no lower-case() in 1.0. Use translate() to map uppercase letters to lowercase and compare against a lowercase literal: translate(normalize-space(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="sign in". Most teams wrap this in a helper function in the test code.

//button[translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')='sign in'] Try →
textfunctions
5 How would you click the Edit button in the table row for a specific user?

Filter the row by its cell content, then descend to the button: //tr[td[normalize-space()="Jane Doe"]]//button[normalize-space()="Edit"]. The predicate on tr contains a path, which is true if the row has such a cell. This does not depend on row order or the number of rows.

//tr[td[normalize-space()='Jane Doe']]//button[normalize-space()='Edit'] Try →
tablespatterns
6 How do you select a table cell by row name and column header without hardcoding the column number?

Compute the column index from the header: count(//th[normalize-space()="Price"]/preceding-sibling::th) + 1, and use it as the td predicate on the row found by content. The locator then survives columns being reordered.

//tr[td[normalize-space()='Widget']]/td[count(//th[normalize-space()='Price']/preceding-sibling::th)+1] Try →
tables
7 What happens when you compare a node-set with != ?

Comparisons against a node-set are existential: they are true if any node satisfies them. So li/@class != "done" is true if at least one li has a different class, which is almost never what you mean. "No li is done" is not(li/@class = "done").

//ul[not(li/@class='done')] Try →
operatorslogic
8 Why does //svg return nothing on a page with inline SVG, and what do you do instead?

The HTML parser puts svg and its children in the SVG namespace, and an unprefixed name test matches only no-namespace elements. Use //*[local-name()="svg"]. Attributes on SVG elements are not namespaced, so @data-icon and @class work normally once the element is matched.

//*[local-name()='svg'][@data-icon='trash']/ancestor::button[1] Try →
svgnamespaces
9 How do you find an input from its label when the label uses the for attribute?

Compare the input id against the node-set of matching label for attributes: //input[@id=//label[normalize-space()="Email"]/@for]. The comparison is true if the id equals any node in the set, so the label and input can be anywhere relative to each other.

//input[@id=//label[normalize-space()='Email']/@for] Try →
forms
10 In Selenium, what is the difference between element.findElement(By.xpath("//a")) and element.findElement(By.xpath(".//a"))?

The first searches the whole document, because an expression starting with // is evaluated from the root regardless of the element it is called on. The second is relative to the element and searches only its subtree. Forgetting the dot is a classic silent bug.

selenium
11 How do you handle an element whose id is generated, such as mat-input-3?

Do not match the id. Use a developer-written attribute (name, type, aria-label), anchor on the label text and walk to the input with following::input[1], or resolve the label's for attribute. If nothing stable exists, starts-with(@id, "mat-input-") plus context is a fallback, and a data-testid is the real fix.

dynamicrobustness
12 What is normalize-space() and when does it not help?

It trims leading and trailing whitespace and collapses internal runs of spaces, tabs and newlines to a single space. It does not touch non-breaking spaces (U+00A0) or zero-width characters, which are not whitespace to XPath; those need translate() or a contains() on a stable substring.

text
13 How do you select the nth match of an expression across the whole page?

Wrap the expression in parentheses and index it: (//button[normalize-space()="Remove"])[2]. Without the parentheses the index applies per parent. Then ask whether the position is really the requirement; usually filtering the container by content is more robust.

(//button[normalize-space()='Remove'])[2] Try →
//li[.//span[normalize-space()='Keyboard']]//button Try →
position
14 How do you escape a string that contains both single and double quotes?

XPath 1.0 has no escape character, so build the literal with concat(), switching quote types around each quote: concat("It", "'", "s ", '"', "quoted", '"'). Test frameworks usually wrap this in a quoting helper.

syntax
15 How do you select an element that does NOT contain a certain child?

Use not() around an existence test: //div[not(.//input)] selects divs with no input anywhere inside. The leading dot in .//input is required inside the predicate; without it the test restarts at the document root.

//form[not(.//input[@type='password'])] Try →
logic

Senior / Lead

Strategy, framework internals, performance, shadow DOM, versions and team conventions.

1 Which XPath version do browsers implement, and what are the practical consequences?

XPath 1.0, through the DOM Level 3 XPath API, unchanged since 2004. No ends-with(), lower-case(), matches(), replace(), sequences or conditional expressions. Every browser-driving tool inherits this. Teams must emulate with translate(), substring() and string-length(), or do the logic in the test language. Online XPath testers that default to 3.1 will accept expressions that fail in the browser.

versions
2 Is XPath slower than CSS? Defend your answer with how a lookup actually spends its time.

In modern browsers both evaluate in fractions of a millisecond on typical pages; the difference is noise next to WebDriver round trips (milliseconds) and waits (seconds). The reputation comes from Internet Explorer, which lacked a native engine. Genuinely expensive patterns are wildcard roots with text predicates (//*[contains(., "x")]) and deep // chains. Choose by capability, and fix waits before selectors when optimising a suite.

performance
3 How does shadow DOM affect XPath, and how do you handle it in Selenium and Playwright?

document.evaluate walks one tree and stops at shadow roots, so XPath cannot enter them in any tool. Selenium 4: locate the host with XPath, call getShadowRoot(), then use CSS (the shadow-root context in Chromium does not accept XPath). Playwright: CSS and role locators pierce open shadow roots automatically; chain an XPath host locator with a CSS inner locator. Closed shadow roots are unreachable by any locator.

shadow-domframeworks
4 Describe a locator strategy for a new Playwright project and where XPath fits.

Test ids on interactive elements and containers by convention; getByRole with accessible names for visible controls; getByLabel for fields; CSS for simple attribute cases; XPath for structural queries (row-by-content, cell-by-header, sibling and ancestor navigation) and for parameterised page-object helpers. Keep XPath in one place with descriptive names, never commit Copy XPath output, and review locators for anchor quality.

strategy
5 What is Playwright strict mode and why is it a feature rather than an annoyance?

An action on a locator that resolves to more than one element throws instead of acting on the first. It catches the modal-versus-page duplicate and the hidden mobile-nav duplicate at the point of failure, where Selenium would silently click the wrong element. The fix is to anchor the locator, or use first()/nth() when order is genuinely the intent.

playwright
6 How do self-healing locator tools work and what policy would you set for them?

They store a fingerprint of each element (tag, attributes, text, neighbours, position) on green runs, and when a locator fails they search the current DOM for the closest match and propose a replacement. Policy: enable in CI, fail the build if a healed locator is reused more than N times without a fix, review replacements against the same rules as hand-written locators, and never let healing act on destructive controls without a text assertion.

trendstooling
7 Why is XPath the locator of last resort on mobile with Appium, and when is it still the right choice?

Each XPath query forces Appium to serialise the entire native view hierarchy to XML, transfer it, evaluate and map results back, which can take hundreds of milliseconds to seconds on complex screens. Accessibility ids, resource ids, iOS class chains and UiAutomator selectors query the platform directly. XPath is still right for text-plus-structure queries the native strategies cannot express, scoped to a container and with element class names instead of wildcards.

appium
8 A test passes locally and fails in CI with a strict mode violation. Walk through your diagnosis.

Read the message: two matches. Most likely a viewport difference rendering both desktop and mobile navigation, or CI seed data producing duplicate rows. Capture DOM and screenshot on failure, load the DOM locally and test the locator, diff against a local DOM, pin the CI viewport, and anchor the locator to the intended landmark or to the test-owned data.

debuggingci
9 How would you design a shared locator library used by Selenium, Playwright and Appium suites?

Store plain XPath strings without tool prefixes, organised per page or component, with parameterised builders for repeated structures and a quoting helper for interpolated text. Add the xpath= or xpath/ prefix at the call site. Validate expressions against a real browser in CI, not an online 3.1 tester. Document anchor conventions and forbid absolute paths and unscoped indexes in review.

strategyarchitecture
10 Explain how an XPath predicate containing another path is evaluated, and the significance of the leading dot.

The inner path is evaluated with the outer step's node as context, and the predicate is true if the inner path selects at least one node. .//x is relative to that context node; //x without the dot restarts at the document root, so //div[//span] is true for every div whenever any span exists anywhere. The dot is the difference between "contains" and "exists somewhere".

predicates
11 What is XPath 4.0 and should your team care?

A draft developed by a W3C community group, not a working group, extending 3.1 with ergonomics such as the otherwise operator, records, string templates and an HTML parser function. It will ship in Saxon, BaseX and XSLT tooling. Browsers are frozen at 1.0, so it has no effect on locators. Relevant only if the team also does XSLT or XML-database work.

versionstrends
12 How do CSS :has() and role-based locators change the case for XPath?

:has() gives CSS "container that contains X", closing part of the structural gap, but CSS still has no text matching, no upward navigation from an inner element to a sibling, and no value results. Role locators handle visible controls better than either. XPath remains necessary for text-plus-structure queries, ancestor and sibling navigation, cross-tool libraries, Selenium, mobile and scraping. Its role narrowed to what only it can do.

strategytrends

Scenario

Whiteboard-style questions: given this markup, write the locator and defend it.

1 Markup: <label for="e1">Email</label> <input id="e1" name="email">. The id changes per build. Write a locator and justify it.

//input[@name="email"] is the simplest, since name is developer-authored. If name is also unreliable: //input[@id=//label[normalize-space()="Email"]/@for], which follows the same association the browser uses, or //label[normalize-space()="Email"]/following::input[1]. Avoid the id.

//input[@name='email'] Try →
//input[@id=//label[normalize-space()='Email']/@for] Try →
formsdynamic
2 A page has a Save button in the form and another in an open dialog. Both say Save. Locate the dialog one.

Anchor on the dialog landmark: //*[@role="dialog"]//button[normalize-space()="Save"]. If several dialogs can be open, add the title: //*[@role="dialog"][.//h2[normalize-space()="Edit profile"]]//button[normalize-space()="Save"]. Avoid (//button[...])[2], which depends on where the dialog is rendered in the DOM.

//*[@role='dialog']//button[normalize-space()='Save'] Try →
anchoringdialogs
3 A product grid renders twenty identical cards, each with an "Add to cart" button. Add the "USB-C Hub" to the cart.

Filter the card by its content, then descend: //article[@data-testid="product-card"][.//h3[normalize-space()="USB-C Hub"]]//button[normalize-space()="Add to cart"]. The leading dot in .//h3 keeps the filter inside each card. No index, no dependence on order.

//article[.//h3[normalize-space()='USB-C Hub']]//button[normalize-space()='Add to cart'] Try →
listspatterns
4 The delete button is icon-only: <button><svg data-icon="trash">...</svg></button>. Locate it.

The svg is namespaced, so match it by local-name and go up to the button: //*[local-name()="svg"][@data-icon="trash"]/ancestor::button[1], or filter the button: //button[.//*[local-name()="svg"][@data-icon="trash"]]. Then suggest the team add aria-label="Delete", which fixes both the locator and accessibility.

//button[.//*[local-name()='svg'][@data-icon='trash']] Try →
svg
5 A table has columns that product managers reorder. Read the Stock value for the Keyboard row.

Find the row by content and the column by header: //tr[td[normalize-space()="Keyboard"]]/td[count(//th[normalize-space()="Stock"]/preceding-sibling::th)+1]. Both halves are content-based, so neither row nor column order matters.

//tr[td[normalize-space()='Keyboard']]/td[count(//th[normalize-space()='Stock']/preceding-sibling::th)+1] Try →
tables
6 Button text is "Sign In" on one environment and "SIGN IN" on another. Write one locator that works on both.

Fold case with translate(): //button[translate(normalize-space(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="sign in"]. Then ask whether a type="submit" or data-testid locator would avoid the text entirely, which is usually the better answer.

//button[translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')='sign in'] Try →
//form[@id='login']//button[@type='submit'] Try →
text
7 You are given //*[@id="root"]/div/div[2]/main/div/form/div[3]/input from a recorder. Rewrite it and explain each change.

Identify what the input is (inspect: say name="postcode" under a Billing heading) and write //main//form//input[@name="postcode"] or //h2[normalize-space()="Billing"]/following::input[@name="postcode"][1]. Changes: drop the root anchor and layout divs (they carry no meaning and change with design), drop the index (breaks on insertion), keep only semantic steps joined by // so wrappers can come and go.

//main//form//input[@name='postcode'] Try →
robustnessrefactoring
8 Locate the checkbox in <label><input type="checkbox" name="tos"> I accept the <a href="/terms">terms</a></label> by its visible text.

The input is a child of the label and the text is mixed with a link, so use contains on the label's string value and step into it: //label[contains(normalize-space(), "I accept the terms")]/input[@type="checkbox"]. normalize-space() on the label concatenates the text and the link text.

//label[contains(normalize-space(), 'I accept the terms')]/input[@type='checkbox'] Try →
formstext
9 Count how many rows in a table are in status "Pending", from a Selenium test, without looping in code.

Use findElements with the row filter and read the size: driver.findElements(By.xpath("//tr[td[normalize-space()='Pending']]")).size(). Or execute count(...) via JavaScript: document.evaluate("count(//tr[td[normalize-space()='Pending']])", document, null, XPathResult.NUMBER_TYPE, null).numberValue. A locator cannot return a number directly.

count(//tr[td[normalize-space()='Pending']]) Try →
seleniumfunctions
10 The login form is inside an iframe from a third-party provider. Describe the locator flow in Selenium and Playwright.

Selenium: driver.switchTo().frame(driver.findElement(By.xpath("//iframe[@title='Login']"))), then normal XPath inside, then switchTo().defaultContent(). Playwright: page.frameLocator("iframe[title=Login]").locator("xpath=//input[@name='user']") with no switching. Locate the frame by title or a stable src fragment, not by index.

iframesframeworks