No XPath access to text nodes


#1

Hi,

it seems to me, that I have no access to text nodes with XPath in Katalon Studio. Though I can identify an element by the content of its child text nodes, I am not able to catch the text nodes themselves. The following example illustrates this:

<div id="node">Text 1<img src="sample.jpg" />Text 2</div>

Actually, I think, here should work …

id("node")/text()[1] 

… to capture the first text node “Text 1”. But it’s not. Is XPath only incompletely implemented in Katalon Studio?

Thanks + regards


#2

Drunda,

I can answer to your question and propose a solution. I will explain in detail taking examples from the following demo project:
https://github.com/kazurayam/KatalonDiscussion6790

# Your problem reproduced

I made a Test Object named ‘Page_sample/dev_node_text1’ with a xpath selector:

id('node')/text()[1]

I made a Test Case named TC2, which is as follows:

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
WebUI.openBrowser('')
WebUI.navigateToUrl('http://demoaut-mimic.kazurayam.com/6790_testbed.html')
def text = WebUI.getText(findTestObject('Page_sample/div_node_text1'))
WebUI.verifyEqual(text, 'Text 1')
WebUI.closeBrowser()

When I run this, it fails. The verifyEqual keyword fails with error a message like this:

Test Cases/TC2 FAILED because (of) Unable to get text of object (Root cause: java.lang.IllegalArgumentException: Object is null)
Test Cases/TC2.run:26

This message tells you that the expression findTestObject_(‘Page_sample/div_node_text1’)_ is evaluated to null. Why null?

# Background knowledge about XPath

The XPath 1.0 specification, 5 Data Model defines 7 types of nodes:

  • root nodes
  • **element **nodes
  • text nodes
  • attribute nodes
  • namespace nodes
  • processing instruction nodes
  • comment nodes

A single XPath expression may return one of these types of nodes.

Your expression :

id("node")/text()[1]

is valid as XPath. If you apply this expression to the problem HTML document using a full-stack XPath engine such as Jaxen, I am sure, this expression would return “Text 1” as you expect.

However, we are not using Jaxen now. We are using Katalon Studio’s findTestObject. So we need to look at findTestObject(String) method. According to the API document, the findTestObject(String) is designed to return an instance of TestObject. What is a TestObject?

**I think a TestObject is designed to wrap an element node only; it can not wrap either a text node nor an attribute node. **

This implicit constraint is not described anywhere in the Katalon documentation, but I have found it by hands-on experiences.

## Rule of thumb:
**
Xpath selector for a Test Object is required to be a valid xpath expression which returns an ‘element node’ as a meaning of XPath. **If you set selector with an xpath which returns a text node, then findTestObject(String ) would return null. A xpath which returns an attribute node would result in null as well.

# Solution proposed

I made a Test Object named ‘Page_sample/dev_node’ with a selector in xpath:

id('node')

I made a Test Case named TS1, which is as follows:

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObjectimport com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI.openBrowser('')WebUI.navigateToUrl('http://demoaut-mimic.kazurayam.com/6790_testbed.html')def text = WebUI.getText(findTestObject('Page_sample/div_node'))WebUI.verifyEqual(text, 'Text 1Text 2')def leadingPart = text.substring(0, text.indexOf('Text 2'))WebUI.verifyEqual(leadingPart, 'Text 1')WebUI.closeBrowser()

This test case runs successful.

What you can get by WebUI.getText() is ‘Text 1Text 2’. The element between ‘Text 1’ and ‘Text 2’ disappears when WebUI.getText(‘Page_sample/div_node’) is evaluated.

You need to parse the ‘Text 1Text 2’ string for yourself with Groovy’s String object methods.

The variable named leadingPart contains ‘Text 1’. This is what you wanted.

Hope this helps.


#3

Hi kazurayam

thanks for your great effort!

But, without having tried it out, if I understand your solution correctly, I would already have to know the complete content of both text nodes, even if I want to access only the first one (say, for example, I want to click it), wouldn’t I?

Thanks + regards!


#4

Another awesome post from Chief Inspector Kazurayum. :slight_smile:


#5

kazurayam,

Maybe your post in this Angular-related thread will also help me in my XPath case, allowing me to simply use the native XPath capabilities of Selenium?

Or does your above approach also allow generic access to any text node (i.e. without knowing its content)? I don’t see how yet.

Regards


#6

Maybe your post in this Angular-related thread will also help me in my XPath case, allowing me to simply use the native XPath capabilities of Selenium?

No.
The Selenium WebDriver interface supports findElement(By) method. This is the only method it supports to get access to the HTML content nodes. The findElement() method returns an instance of WebElement which wraps a element-node of XPath. This is the very reason why Katalon Studio’s Test Object is desined to wrap only the element-node.

You can not get access to the text nodes in the meaning of XPath via the WebDriver API. In other words, WebDriver API does not expose native XPath capabilities.


#7

A workaround. If you are able to modify the target web app, you want to change the HTML document :

<div id="node">  <span>Text 1</span>  <img src="sample.jpg" />  <span>Text 2</span></div>Then the following xpath would be your easiest solution:id("node")/span[1]Sometimes changing the target web app is the best solution for testing problems. We should design our web app with testing in mind. ;) 


#8

Hi kazurayam,

A workaround. If you are able to modify the target web app, you want to change the HTML document :
[…]

Thank you, of course, I was aware of that. :wink:
And as a consequence, I will probably include this hint in our coding guide. In my actual case it was fortunately sufficient to use just clickOffset instead of only click. :slight_smile:

Thanks + regards