下面的信息使用浏览器测试工具作为示例,但是同样的概念也适用于在 XML 工具中创建自定义 XPath(XML 断言器、数据库等)。
SOAtest 生成用于浏览器测试的元素定位器;这些定位器允许 SOAtest 在回放期间找到适当的元素,以便 SOAtest 能够对该元素执行用户操作或验证该元素的属性。SOAtest 试图创建一个定位器,在对 web 页面进行少量更改之后仍然可以工作。但是,有时需要创建自定义元素定位器。SOAtest 可能没有足够的信息来生成一个健壮的定位器。
在为复杂的 HTML 页面创建自定义 Xpath 表达式时,更容易在部件中创建 XPath 并使用呈现的视图来验证是否找到了所需的元素。如果从一个复杂的 XPath 表达式开始,并且没有突出显示任何内容,那么确定 XPath 的哪些部分不起作用会十分困难。
如果需要创建参数化 Xpath,首先创建一个文本 XPath,并验证它是否突出显示,并在回放期间找到正确的元素。然后,创建一个脚本来参数化 XPath。同样,这简化了调试。
创建自定义元素定位器
创建自定义元素定位器的一个常见用例是在处理表时。例如,您可能希望根据该行中另一列的内容点击表中的链接。您可能希望总是点击表的最后一行中大小可能不同的内容。本节概述如何为这些情况创建自定义 XPath 定位器。
若要创建自定义 XPath 表达式,需要检查给定页面的 HTML。考虑具有以下表格的页面。
<table border="1"> <thead> <th>number</th> <th>comment</th> <th>details link</th> <th>other link</th> </thead> <tbody id="content"> <tr> <td>000</td> <td>nothing special</td> <td><a href="#" id="details000" onclick="foo()">Details</a></td> <td><a href="#">Other</a></td> </tr> <tr> <td>123</td> <td>nothing special</td> <td><a href="#" id="details123" onclick="foo()">Details</a></td> <td><a href="#">Other</a></td> </tr> <tr> <td><em>456</em></td> <td>formatting: nodes within column</td> <td><a href="#" id="details456" onclick="foo()">Details</a></td> <td><a href="#">Other</a></td> </tr> <tr> <td> 789 + 1 </td> <td>whitespace</td> <td><a href="#" id="details789" onclick="foo()">Details</a></td> <td><a href="#">Other</a></td> </tr> <tr> <td>888</td> <td>find my comment</td> <td><a href="#" id="details888" onclick="foo()">Details</a></td> <td><a href="#">Other</a></td> </tr> <tr> <td>999</td> <td>last row</td> <td><a href="#" id="details999" onclick="foo()">Details</a></td> <td><a href="#">Other</a></td> </tr> </tbody> </table>
以下为该表的 ASCII 形式:
+-----------------------------------------------------------------------+ | number | comment | details link | other link | |---------+---------------------------------+--------------+------------| | 000 | nothing special | Details | Other | |---------+---------------------------------+--------------+------------| | 123 | nothing special | Details | Other | |---------+---------------------------------+--------------+------------| | 456 | formatting: nodes within column | Details | Other | |---------+---------------------------------+--------------+------------| | 789 + 1 | whitespace | Details | Other | |---------+---------------------------------+--------------+------------| | 888 | find my comment | Details | Other | |---------+---------------------------------+--------------+------------| | 999 | last row | Details | Other | +-----------------------------------------------------------------------+
在表中查找元素
假设您想点击数字列中“123”行的“详情”链接。
下面的 Xpath 将用于当前表:
/descendant::a[text()='Details'][2]
这将在文档中找到第二个带有文本“详情”的链接。如果在“123”行之前添加任何带有文本“详情”的附加链接,则此值不会计算到所需的元素。
下面是一个更健壮的 XPath 表达式,可以点击“123”行中的“详情”链接:
//tbody[@id='content']/tr[td[1]/text() = '123']/descendant::a[text()='Details']
下面是 XPath 表达式的每个部分:
//tbody[@id='content']
: 在文档中查找<tbody>
元素,该文档的id
元素等于 "content"。其中,“tbody”是元素的标记名。使用@id
引用一个名为“id”的属性。方括号 "[]
"定义一个谓词,可以将其视为搜索条件。在谓词中,上下文节点是<tbody>
元素:@id
指的是<tbody>
元素的属性。/tr[td[1]/text() = '123']
: 在第一列的文本等于“123”的<tbody>
元素中查找子<tr>
行。正斜杠(/
)指的是<tr>
元素时<tbody>
的子元素。- 谓词
[td[1]/text() = '123']
:<td>
必须是<tr>
的子元素,因为<tr>
是上下文节点。对于 "td[1]","[1]” 谓词指定使用第一个子元素<td>
,也就是第一列中的单元格。如果更改表中列的顺序,使“number”列成为第二列,则需要将其更改为“td[2]”。 /text() = '123'
: 使用值“123”匹配<td>
元素的子文本节点。/descendant::a[text()='Details']
: 使用给定文本查找<tr>
元素中包含的<a>
元素。“descendant::”指定要使用的轴,其中轴定义要使用的节点之间的关系。“child”轴为默认轴:“/x”类似于“/child::x”。这个“descendant”访问包含所有的子、孙、曾孙等。W3C 推荐如下定义“descendant”轴:“后代轴包含上下文节点的子代;后代是子或者孙,以此类推;因此,后代轴从不包含属性或名称空间节点。"
可使用以下坐标轴。关于更多信息,请参阅 W3C 推荐。很可能您会主要使用“child”轴和“descendant”轴。
- ancestor
- ancestor-or-self
- attribute
- child
- descendant
- descendant-or-self
- following
- following-sibling
- namespace
- parent
- preceding
- preceding-sibling
- self
如果 Number 列包含元素
“456”行的“number”列的元素:
<td><em>456</em></td>
则只需稍加修改就可以使用前面的 XPath。如果使用谓词 [td[1]/text() = '456'],则不会找到任何行。原因是 td[1]/text() 计算 <td> 元素的子文本节点。这里的 test 节点是后代节点,但不是 <td> 元素的子元素。您可以使用谓词 [td[1]/descendant::text() = '456']。(您原本可以在这里使用 (1) 的 XPath 中的后代轴,因为子节点就是后代节点。)
但是,假设 <td> 元素包含了多个 text 节点,大致如下:
<td>order <em>456</em></td>
这个示例是人为设计的,但是在更复杂的表中,表单元格的可见文本有可能跨越多个文本节点。如果是这样的话,谓词 [td[1]/descendant::text() = 'order 456'] 无法工作。表达式 td[1]/descendant::text() 计算为文本节点“order”和“456”;没有一个等于“order 456”。也就是说,test() 计算 text 节点的集合,而不是连接那些 text 节点。
相反,您可以使用谓词 [td[1] = 'order 456']。当您将节点与文本值进行比较时,该节点将转换为字符串。W3C 推荐指定“元素节点的字符串值是元素节点的所有文本节点后代的字符串值按文档顺序串联起来”,这正是您想要的。
回到 <td><em>456</em></td> 的原始表行,您可以使用这个 Xpath 点击编号为“456”行的“详情”链接:
//tbody[@id='content']/tr[td[1] = '456']/descendant::a[text()='Details']
注意,您 也可以对“123”行使用这种形式的 XPath。可以说,这种形式更健壮。
处理空白
"789 + 1” 行包含数字列的单元格:
<td> 789 + 1 </td>
谓词 [td[1] = '789 + 1'] 将不会找到任何行,因为在 XPath 表达式中空格很重要。您需要包括所有的空格字符,以让其工作:[td[1] = ' 789 + 1 '].但是,如果空白发生了变化,这将不起作用,请注意 HTML 中的换行也是重要的空白。您可以使用 normalize-space 函数忽略空格中的波动,根据 W3C 的建议,该函数“通过去掉前面和后面的空格并将空格序列替换为单个空格,从而用规范化的空格返回参数字符串”。
因此,可以使用以下 XPath:
//tbody[@id='content']/tr[normalize-space(td[1])='789 + 1']/descendant::a[text()='Details']
还存在许多其他 XPath 字符串函数,比如 concat、substring 和 contains。关于更多信息,请参阅 W3C 推荐。
处理列的重新排序
无论“details link”列在表中的什么位置,前面的 XPath 表达式都可以工作。但是,表达式假定“number”列总是第一列。若要处理移动的“number”列,需要修改谓词 td[1]。谓词必须包含表 <thead> 元素中的“number ”数据头单元格的索引,而不是 "[1]"。
但首先:如果满足于在所有列中搜索给定的值,那么就可以简单地删除 <td> 元素上的索引谓词。若要查找行“888”,请执行以下操作:
//tbody[@id='content']/tr[td = '888']/descendant::a[text()='Details']
但是请注意,如果字符串“888”作为文本作为任何列中的另一个表单元格出现,那么 XPath 可能不会返回您想要的结果。如果想避免这个问题,请执行以下操作。
对于本例,使用“comment”列来查找编号为“888”的行,以演示 XPath 不会在第一列中查找。
如果搜索行“888”,可以使用以下 XPath:
//tbody[@id='content']/tr[td[count ( ancestor::table/thead/th[.='comment']/preceding-sibling::th ) + 1] = 'find my comment']/descendant::a[text()='Details']
Xpath 与前面的示例相同,除了 <td> 元素的这个新谓词:
td[count ( ancestor::table/thead/th[.='comment']/preceding-sibling::th ) + 1]
这个谓词在表中搜索 <thead> 元素,查找具有值“comment”的表单元格,然后确定该行的索引。
- ancestor::table/thead : 使用“ancestor”坐标轴来查找存在 <td> 上下文节点的 <table> 元素。然后使用表的 <thead> 子元素。
- th[.='comment'] : 找到文本等于“comment”的表格单元格。这里的“.”是 self::node()的简单表示法,其计算 <th> 元素自身。然后将元素转换为字符串值,因为您将它与字符串“comment”进行比较。之所以可以这样做(而不是使用 td[text()='comment']),是因为 <th> 元素只包含一个 text 节点。
/preceding-sibling::th : 查找 <thead> 元素的所有 <th> 子元素,并且显示在“comment”列(上下文节点)之前。因为“comment”列是当前的第二列,这将找到一个元素。如果将列按照以下方式重新排列:
<thead> <th>details link</th> <th>other link</th> <th>number</th> <th>comment</th> </thead>
则 /preceding-sibling::th 将找到三个元素。- count ( ancestor::table/thead/th[.='comment']/preceding-sibling::th ) + 1
Count 函数返回通过计算输入表达式返回的节点数。在本例中,它返回在 <th>comment</th> 之前显示的 <th> 列数据头单元格的编号。将 1 添加到 count 中,因为 XPath 索引是基于 1 的:如果这是第一列,那么 count 返回 0。
这个 XPath 表达式比您通常想要或需要的更复杂,但是它演示了什么是可能的。
如果您在使用浏览器计算 Xpath 表达式时遇到性能问题,请参阅为提高的性能或支持切换 XPath 库以了解使用替代 Xpath 库的方法。还可以尝试脚本化元素定位器(参考Scripting Element Locators)。
找到最后一行
想要点击最后一行中的“详情”链接,而不考虑“number”列中的值。您可以使用该 XPath:
//tbody[@id='content']/tr[last()]/descendant::a[text()='Details']
tr[last()] 为此返回 <tbody> 中的最后一个<tr> 子元素。
如果想用倒数第二行(假设该表至少包含两行),则可以使用 tr[last() - 1]。有关 last() 和 position() 函数的更多信息,请参阅 W3C 推荐标准。
使用带有参数化值的 XPath
在 SOAtest 中,您可以使用参数化的值,该值是来自数据源、测试逻辑变量或提取的值。若要创建引用参数化值的 XPath 表达式,则需要创建脚本化元素定位器。
假设您想要基于先前测试中提取的“number”值点击“详情”链接。例如,可能该行包含一个由以前的用户操作生成的新数字。那么您可以使用浏览器数据库从以前的浏览器测试中提取新的数字。要创建一个浏览器测试,请点击所生成编号指定的链接,使用“配置浏览器测试”来使用“用户动作”选项卡中的“元素定位器”值。然后可以使用以下 JavaScript 脚本:
var Application = Packages.com.parasoft.api.Application; // input: com.parasoft.api.BrowserContentsInput. // context: webking.api.BrowserTestContext. function scriptedXpath(input, context) { // Assumes that you configured the extraction to use column name "extractedNumber". // Use data source name "", null, or "Generated Data Source" because the // value is from an extraction, not a data source (table, CSV, etc.). var number = context.getValue("", "extractedNumber"); var xpath = "//tbody[@id='content']/tr[td[1] = '" + number + "']/descendant::a[text()='Details']"; // Output XPath for debugging purposes. Application.showMessage("scripted XPath: " + xpath); return xpath; }
脚本元素定位器
如果需要使用复杂的逻辑来查找元素,则单个 XPath 表达式可能会变得笨拙。注意,使用脚本化元素定位器时,可以返回 XPath 字符串或 org.w3c.dom.Node 对象。如前所述,IE 计算复杂 XPath 表达式可能很慢;使用返回节点的脚本定位器可能更快。
在最后一行找到“详情”链接的脚本:
var WebBrowserTableUtil = Packages.webking.api.browser2.WebBrowserTableUtil; // input: com.parasoft.api.BrowserContentsInput. // context: webking.api.BrowserTestContext. function scriptedNode(input, context) { var document = input.getDocument(); var tbody = document.getElementById("content"); // Assumes that there are no rows within rows: this returns // all descendants with the given tag, not just children. var rows = tbody.getElementsByTagName("tr"); var lastRow = rows.item(rows.getLength() - 1); var detailsColumnIndex = 2; // Zero-based. var detailsLink = WebBrowserTableUtil.getLinkFromCell( detailsColumnIndex, lastRow); return detailsLink; }
找到正确的元素
如果希望在 web 页面上找到元素,请添加允许找到所需元素的属性。例如,在这些示例中使用的 <tbody> 元素有一个唯一的“id”属性值。如果该页包含多个表,并且 <tbody> 和 <table> 都没有包含“id”属性(或其他属性的组合,以使您能够唯一地标识表),则很难创建使用该表的简单 XPath 表达式。此外,如果需要定位器的元素(例如,您需要点击的链接)已经有一个“id”属性,则您可以创建一个“Use Element Properties”的定位符,或者创建一个非常简单的 Xpath, //a[@id='value']。