XPath: difference between dot and text() - selenium

My question is about specifics of using dot and text() in XPath. For example, following find_element lines returns same element:
driver.get('http://stackoverflow.com/')
driver.find_element_by_xpath('//a[text()="Ask Question"]')
driver.find_element_by_xpath('//a[.="Ask Question"]')
So what is the difference? What are the benefits and drawbacks of using . and text()?

There is a difference between . and text(), but this difference might not surface because of your input document.
If your input document looked like (the simplest document one can imagine given your XPath expressions)
Example 1
<html>
<a>Ask Question</a>
</html>
Then //a[text()="Ask Question"] and //a[.="Ask Question"] indeed return exactly the same result. But consider a different input document that looks like
Example 2
<html>
<a>Ask Question<other/>
</a>
</html>
where the a element also has a child element other that follows immediately after "Ask Question". Given this second input document, //a[text()="Ask Question"] still returns the a element, while //a[.="Ask Question"] does not return anything!
This is because the meaning of the two predicates (everything between [ and ]) is different. [text()="Ask Question"] actually means: return true if any of the text nodes of an element contains exactly the text "Ask Question". On the other hand, [.="Ask Question"] means: return true if the string value of an element is identical to "Ask Question".
In the XPath model, text inside XML elements can be partitioned into a number of text nodes if other elements interfere with the text, as in Example 2 above. There, the other element is between "Ask Question" and a newline character that also counts as text content.
To make an even clearer example, consider as an input document:
Example 3
<a>Ask Question<other/>more text</a>
Here, the a element actually contains two text nodes, "Ask Question" and "more text", since both are direct children of a. You can test this by running //a/text() on this document, which will return (individual results separated by ----):
Ask Question
-----------------------
more text
So, in such a scenario, text() returns a set of individual nodes, while . in a predicate evaluates to the string concatenation of all text nodes. Again, you can test this claim with the path expression //a[.='Ask Questionmore text'] which will successfully return the a element.
Finally, keep in mind that some XPath functions can only take one single string as an input. As LarsH has pointed out in the comments, if such an XPath function (e.g. contains()) is given a sequence of nodes, it will only process the first node and silently ignore the rest.

There is big difference between dot (".") and text() :-
The dot (".") in XPath is called the "context item expression" because it refers to the context item. This could be match with a node (such as an element, attribute, or text node) or an atomic value (such as a string, number, or boolean). While text() refers to match only element text which is in string form.
The dot (".") notation is the current node in the DOM. This is going to be an object of type Node while Using the XPath function text() to get the text for an element only gets the text up to the first inner element. If the text you are looking for is after the inner element you must use the current node to search for the string and not the XPath text() function.
For an example :-
<a href="something.html">
<img src="filename.gif">
link
</a>
Here if you want to find anchor a element by using text link, you need to use dot ("."). Because if you use //a[contains(.,'link')] it finds the anchor a element but if you use //a[contains(text(),'link')] the text() function does not seem to find it.
Hope it will help you..:)

enter image description here
The XPath text() function locates elements within a text node while dot (.) locate elements inside or outside a text node. In the image description screenshot, the XPath text() function will only locate Success in DOM Example 2. It will not find success in DOM Example 1 because it's located between the tags.
In addition, the text() function will not find success in DOM Example 3 because success does not have a direct relationship to the element . Here's a video demo explaining the difference between text() and dot (.) https://youtu.be/oi2Q7-0ZIBg

Related

XPath: difference between "//*[contains(.,'sometext')]" and "//*[contains(text(),'sometext')]" [duplicate]

This question already has answers here:
Why is XPath contains(text(),'substring') not working as expected?
(2 answers)
XPath contains(text(),'some string') doesn't work when used with node with more than one Text subnode
(7 answers)
Testing text() nodes vs string values in XPath
(1 answer)
Closed 3 months ago.
I want to clearly understand what is the difference between the following XPath expressions "//*[contains(.,'sometext')]" and "//*[contains(text(),'sometext')]".
From this great answer I understand, that text() returns a set of individual nodes, while . in a predicate evaluates to the string concatenation of all text nodes.
OK, but when I'm using [contains(.,'sometext')] or [contains(text(),'sometext')] this should return the same amount of elements matching those XPaths since here we checking for nodes containing someText content in itself or in some of their children. Right? And it doesn't matter if we are checking whether any of the text nodes of an element contains sometext or string concatenation of all text nodes contains the sometext text. This should give the same amount of matches.
However if we test this for example on this page I see 104 matches for //*[contains(text(),'selenium')] XPath while //*[contains(.,'selenium')] XPath is giving 442 matches.
So, what causes this difference?
Let me share my understanding using this xml.
<test>
<node>
selenium
<node2>
selenium
</node2>
</node>
<node>
selenium
</node>
</test>
First of all function text() returns list of node objects.
Function contains() takes two arguments where the first one is a string. So having this //*[contains(text(),'selenium')] would not always work. In XPath v2.0 It will fail when text() supplies several nodes to contains.
In my mentioned example white spaces before nodes are also text node:
This is why in my test your //*[contains(text(),'selenium')] query failed. Probably browsers have some work around for that to make things easier.
Now lets collapse that xml to get rid of that noise and look at the differences of approaches:
<test><node>selenium<node2>selenium</node2></node><node>selenium</node></test>
1. use text().
Here what https://www.freeformatter.com/xpath-tester.html returns:
Element='<node>selenium<node2>selenium</node2>
</node>'
Element='<node2>selenium</node2>'
Element='<node>selenium</node>'
Since //* defines all nodes within the tree here we have /test/node[1] that contains, also /test/node[1]/node2 and /test/node[2].
2. Now lets look at . case:
Now it returns:
Element='<test>
<node>selenium<node2>selenium</node2>
</node>
<node>selenium</node>
</test>'
Element='<node>selenium<node2>selenium</node2>
</node>'
Element='<node2>selenium</node2>'
Element='<node>selenium</node>'
Why? because first of all /test is converted to seleniumseleniumselenium. Then /test/node[1] is converted to seleniumselenium, then /test/node[1]/node2 is converted to selenium and finally /test/node[2] is converted to selenium
So this makes the difference. Depending on how complex your nesting is, the results might show more or less significant difference between to approaches.
This thread exlains the difference between dot and text() pretty well: XPath: difference between dot and text()

How to find the xpath for an element by text containing three dots

I'm trying to find the xpath for an element that contains three dots instead of text. Once I expand the element by clicking on arrow in DOM model html it returns the text.
The following xpath returns nothing unfortunately:
//button[contains(text(),'Pending')]
What's the right solution?
The text content of that element is not a three dots but Pending (). You see the ... only because there is no room to present the text there until you expand the element. So, generally this //button[contains(text(),'Pending')] should work.
You can also try this XPath expression:
//button[contains(.,'Pending')]
It means "a button element with any attribute containing Pending value"
It is more general and should work.
To understand why //button[contains(text(),'Pending')] did not work see this post or this. There are more explanations about this.

Is there a way to find any elements by text in Capybara?

I want to find any element with a given text on my page, but when I pass it to find without an element it gives me back an error
find(material.attachment_filename) #material.attachment_filename is "01. pain killer.mp3"
But if i do:
find('a',text: material.attachment_filename)
It works fine, and the given error is:
Selenium::WebDriver::Error::InvalidSelectorError:
Given css selector expression "01. pain killer.mp3" is invalid: SyntaxError: An invalid or illegal string was specified
Capybara's find takes 3 arguments (a Capybara selector type, a locator string, and an options hash). If the selector type isn't specified it defaults to :css which means the locator string needs to be a CSS selector.
This means that find(material.attachment_filename) in your case is equivalent to
find(:css, "01. pain killer.mp3")
which will raise an error as you've seen because "01. pain killer.mp3" isn't valid CSS. If you want to find any element containing the text you could do something like
find('*', text: "01. pain killer.mp3")
which will find any element containing the text, however that's also going to find all the ancestor elements too since they also contain the text, So what you'd probably want is to use a regex to make sure the element contains only that content
find('*', /\A#{Regexp.escape(material.attachment_filename)}\z/)
which should be interpreted as
find('*', /\A01\. pain killer\.mp3\z/)
Note: That is going to be pretty slow if your page has anything more than simple content on it because it means transferring all the elements from selenium to capybara to check the text content.
A more performant solution would be to use XPath which has support for finding elements by text content (CSS does not)
find(:xpath, XPath.descendant[XPath.string.n.is(material.attachment_filename)]) #using the XPath library - contains (assuming Capybara.exact == false)
find(:xpath, XPath.descendant[XPath.string.n.is(material.attachment_filename)], exact: true) #using the XPath library - equals (you could also pass exact:false to force contains)
If the text won't contain XPath special characters (ex. apostrophes) that need escaping, you can use a string to define the XPath:
find(:xpath, ".//*[contains(., '#{material.attachment_filename}')]") #contains the text
find(:xpath, ".//*[text()='#{material.attachment_filename}']") #equals the text
If the element is actually a link you're looking for though then you would probably want to use
find_link("01. pain killer.mp3")
or
find(:link, "01. pain killer.mp3")

XPath - not able to find an element with children text

<a>
This is <var>Me</var> and That is <var> You</var>
</a>
I can find an element "a" which contains "This is" by following code:
//a[contains(text(),'This is')]
But I am not able to find element "a" which contains "This is Me and That is You".
//a[contains(text(),'This is Me and That is You')]
Is there a way to find an element with children text as well?
I am not sure if this what you need but you can use string() to get the result as required,
//a[string()='This is Me and That is You']
The caveat however will be that you need to have precised information about the String being used.
See working example here.
This also can be find using normalize-space() function of xpath which strips leading and trailing white-space from a string, replaces sequences of whitespace characters by a single space, and returns the resulting string as below :-
//a[normalize-space()='This is Me and That is You']

Selenium and XPath - locating a link by containing text

I'm trying to use XPath to find an element containing a piece of text, but I can't get it to work....
WebElement searchItemByText = driver.findElement(By.xpath("//*[#id='popover-search']/div/div/ul/li[1]/a/span[contains(text()='Some text')]"));
If I remove the last bit with the "contains" thing, it locates my span element, but I need to select it based on the text contents. It's not going to be a perfect match of 'Some text' either, because it might contain truncated strings as well.
What is the issue?
I think the problem is here:
[contains(text()='Some text')]
To break this down,
The [] are a conditional that operates on each individual node in
that node set -- each span node in your case. It matches if any of the individual nodes it operates
on match the conditions inside the brackets.
text() is a selector
that matches all of the text nodes that are children of the context
node -- it returns a node set.
contains is a function that operates
on a string. If it is passed a node set, the node set is converted
into a string by returning the string-value of the node in the
node-set that is first in document order.
You should try to change this to
[text()[contains(.,'Some text')]]
The outer [] are a conditional that operates on each individual node
in that node set text() is a selector that matches all of the text
nodes that are children of the context node -- it returns a node
set.
The inner [] are a conditional that operates on each node in that
node set.
contains is a function that operates on a string. Here it is passed
an individual text node (.).
Use this
//*[#id='popover-search']/div/div/ul/li[1]/a/span[contains(text(),'Some text')]
OR
//*[#id='popover-search']/div/div/ul/li[1]/a/span[contains(.,'Some text')]
#FindBy(xpath = "//button[#class='btn btn-primary' and contains(text(), 'Submit')]") private WebElementFacade submitButton;
public void clickOnSubmitButton() {
submitButton.click();
}
Use:
#FindBy(xpath = "//span[#class='y2' and contains(text(), 'Your Text')] ")
private WebElementFacade emailLinkToVerifyAccount;