以下のセクションでは、例としてブラウザー テスト ツールを使用しますが、XML Assertor や Data Bank などの XML ツールでカスタム XPath を使用する場合にも同じ概念が適用されます。

SOAtest は、要素ロケーターを生成してブラウザー テストで使用します。これらのロケーターを使用して、SOAtest はシナリオの再生中に適切な要素を探し、要素に対してユーザー アクションを実行したり、要素の属性を検証するといった処理を行います。SOAtest は、Web ページに僅かな変更があった場合でも動作するようなロケーターを作成しようとします。しかし、ときにはカスタム要素ロケーターを作成する必要が発生します。SOAtest が変化に強いロケーターを生成できるだけの情報を持っていない場合もあります。

複雑な HTML ページに対してカスタム XPath 式を作成する場合、少しずつ XPath を作成し、レンダー ビューを使用して目的の要素を発見できているかを確認すると作業が容易です。初めから複雑な XPath 式を作成したが、何もハイライトされていない場合、XPath のどの部分が誤っているのかを判断するのは容易ではありません。

パラメータライズされた XPath を作成する必要がある場合、まずリテラルな XPath を作成し、XPath がハイライトされ、シナリオ再生中に正しい要素を発見できているかを確認します。その後、スクリプトを作成して XPath をパラメータライズします。これもデバッグを容易にするための方法です。

カスタム要素ロケーターの作成

カスタム要素ロケーターを作成する場合によくあるユースケースの 1 つは、表を扱うケースです。たとえば、表のある列のコンテンツに応じて同じ行の他の列にあるリンクをクリックする場合などです。また、サイズが一定ではない表の最後の行にあるものをクリックしたい場合もあるかもしれません。このセクションでは、このような場合にカスタム 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>


この表を文字で表すと次のようになります。

+-----------------------------------------------------------------------+
| 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 |
+-----------------------------------------------------------------------+

表の要素を探す

number 列が "123" の行の "Details" リンクをクリックしたいものとします。

次の XPath は現在の表に対しては有効です。

/descendant::a[text()='Details'][2]

この XPath は、ドキュメントの "Details" というテキストを持つ 2 つ目の リンクを検索します。仮に "123" 行の前に "Details" というテキストを持つリンクがさらに追加された場合、XPath 式の結果は目的の要素を指し示しません。

"123" 行の "Details" リンクをクリックするための、より変化に強い XPath 式は次のとおりです。

//tbody[@id='content']/tr[td[1]/text() = '123']/descendant::a[text()='Details']

XPath 式の各部分の説明は以下のとおりです。

  • //tbody[@id='content'] : ドキュメントの任意の場所にある id 要素が "content" に等しい <tbody> 要素を検索します。"tbody" は要素のタグ名です。@id を使用して "id" という名前の属性を参照します。角括弧 "[]" は述語を定義します。述語は検索条件であると考えることができます。述語内では、コンテキスト ノードは <tbody> です。@id は <tbody> 要素の属性を指します。
  • /tr[td[1]/text() = '123'] : <tbody> 要素内の最初の列のテキストが "123" に等しい子 <tr> 要素を検索します。"/" は <tr> 要素が <tbody> 要素の子であることを指定しています。
  • 述語 [td[1]/text() = '123'] : <tr> がコンテキスト ノードであるため、<td> は <tr> 要素の子でなければなりません。"td[1]" については、述語 "[1]" によって最初の子 <td> 要素を使用するよう指定しています。つまり、最初の列のセルです。表の列の順序を変更し、"number" 列が 2 つめの列になった場合、"td[2]" に変更する必要があります。
  • /text() = '123' : 値 "123" を持つ <td> 要素の子テキスト ノードに一致します。
  • /descendant::a[text()='Details'] : <tr> 要素内の任意の位置にある特定のテキストを持つ <a> 要素を検索します。"descendant::" は使用する軸を指定しています。軸とは、ノード間の関係を定義します。"child" 軸がデフォルトの軸です。"/x" は "/child::x" と同じです。"descendant" はすべての子、孫、ひ孫等を含みます。W3C 勧告では、"descendant" 軸は次のように定義されています。「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> 要素の子テキスト ノードに一致するからです。この例では、テキスト ノードは <td> 要素の子ではなく孫です。そのため、[td[1]/descendant::text() = '456'] という術語を使用します (前の例でも descendant 軸を使用できます。なぜなら、子も子孫だからです)。

しかし、たとえば次のように <td> 要素に複数のテキスト ノードがある場合はどうでしょうか。

<td>order <em>456</em></td>

この例は作為的に作成したものですが、複雑な表では、表のセルに表示されるテキストが複数のテキスト ノードで構成される可能性も十分にあるでしょう。その場合、[td[1]/descendant::text() = 'order 456'] という術語は有効ではありません。td[1]/descendant::text() という式はテキスト ノード "order " および "456" に一致しますが、どちらも "order 456" に等しくありません。つまり、text() はテキスト ノードの集合に一致しますが、連結されたテキスト ノードには一致しません。

代わりに [td[1] = 'order 456'] という述語を使用できます。ノードをテキスト値と比較すると、ノードは文字列に変換されます。W3C 勧告は「要素ノードの文字列値は、要素ノードのすべての子孫テキスト ノードをドキュメントの順番に連結したものである」と規定しており、これが目的の値です。

元のセル <td><em>456</em></td> に戻って考えるなら、次の XPath を使用して number 列が "456" の行にある "Details" リンクをクリックできます。

//tbody[@id='content']/tr[td[1] = '456']/descendant::a[text()='Details']

この形式の XPath は "123" 行に対しても使用できることに注意してください。おそらく、この形式のほうがより変化に強いでしょう。

空白を処理する

"789 + 1" の行の number 列には次のセルが含まれます。

<td> 789  + 1  </td>

[td[1] = '789 + 1'] という述語は、どの行にも一致しません。なぜなら、 XPath 式では空白も考慮されるからです。すべての空白文字を含める必要があります。そのため、[td[1] = ' 789 + 1 '] という式は有効です。[td[1] = ' 789 + 1 '].しかし、空白が変化すると有効ではなくなるほか、HTML の改行も空白として考慮されることに注意が必要です。normalize-space 関数を使用すると、空白の揺らぎを無視できます。W3C 勧告の定義では、normalize-space 関数 は「引数の文字列から先頭および末尾の空白を削除し、連続する空白文字を 1 つの空白に変換することで空白を正規化して返します。」

そのため、次の XPath を使用できます。

//tbody[@id='content']/tr[normalize-space(td[1])='789 + 1']/descendant::a[text()='Details']

そのほかにも、concat、substring、contains などの XPath 文字列関数があります。詳細については W3C 勧告を参照してください。詳細については W3C 勧告を参照してください。

列の順序変更を処理する

これまでの XPath 式は、"details" リンク列が表のどこにあっても有効です。しかし、式は "number" 列が常に最初の列であると仮定しています。"number" 列の位置の変更に対処するためには、td[1] という術語を変更する必要があります。"[1]" ではなく、述語に表の <thead> 要素にある "number" ヘッダー セルのインデックスを含める必要があります。

しかしその前に: 特定の値を探すためにすべての列を検索するのでもよい場合は、単に <td> 要素のインデックス述語を削除することができます。行 "888" を探す場合、次のようになります。

//tbody[@id='content']/tr[td = '888']/descendant::a[text()='Details']

ただし、文字列 "888" が他の列のセル内にテキストとして出現する場合、この XPath は目的の列を返しません。この問題を防ぐには、次のようにします。

このサンプルでは、XPath が最初の列を検索していないということを示すため、"comment" 列を使用して番号 "888" の行を探します。

行 "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" という文字列と比較しているため、要素が文字列に変換されます。<th> 要素にはテキスト ノードしか含まれていないため、代わりに td[text()='comment'] を使用することもできます。
  • /preceding-sibling::th : <thead> 要素の子であり、"comment" 列 (コンテキスト ノード) よりも前に出現するすべての <th> 要素を検索します。現在、"comment" 列は 2 番目の列であるため、1 つの要素が見つかります。次のように列が入れ換えられたとします。


    <thead>
      <th>details link</th>
      <th>other link</th>
      <th>number</th>
      <th>comment</th>
    </thead>


    すると /preceding-sibling::th は 3 つの要素に一致します。

  • count ( ancestor::table/thead/th[.='comment']/preceding-sibling::th ) + 1

count 関数は、評価している入力式から返されたノードの数を返します。この例では、<th>comment</th> の前に出現する <th> 列ヘッダーセルの数を返します。XPath インデックスは 1 から開始するため、count 関数の結果に 1 を加算します。"comment" 列が最初の列である場合、count 関数は 0 を返します。

この XPath 式は、通常に必要とされる以上に複雑ですが、XPath 式によって何ができるかを示しています。

Internet Explorer で XPath 評価のパフォーマンスの問題が発生した場合、「パフォーマンスまたはサポートの改善のために XPath ライブラリを変更」を参照し、別の XPath ライブラリを使用する方法を確認してください。また、スクリプトによる要素ロケーター作成を試してください (「Scripting Element Locators」を参照)。

最終行を取得する

"number" 列の値にかかわらず、最終行の "Details" リンクをクリックしたい場合、次の XPath を使用します。

//tbody[@id='content']/tr[last()]/descendant::a[text()='Details']

tr[last()] という術語は、<tbody> 内の最後の <tr> 子要素を返します。

最後から 2 番目の行 (表には少なくとも 2 行以上あると仮定して) を使用するには、tr[last() - 1] を使用します。last() および position() 関数の詳細については W3C 勧告を参照してください。

パラメータライズされた値で XPath を使用する

SOAtest では、パラメータライズされた値を使用できます。パラメータライズされた値とは、データ ソースやテスト ロジック変数から取得された値、または抽出によって取得された値です。パラメータライズされた値を参照する XPath 式を作成するには、スクリプトによる要素ロケーターを作成する必要があります。

前のテストで抽出された "number" 値に基づいて "Details" リンクをクリックするケースを考えてみます。たとえば、前のユーザー アクションによって生成された新しい番号が行に含まれる場合などです。前のブラウザー テストで Browser Data Bank を使用することで、新しい番号を抽出できます。その生成された番号のリンクをクリックするブラウザー テストを作成するには、ブラウザー テストの [ユーザー アクション] タブで、スクリプトによる [要素ロケーター] 値を使用するよう構成します。その後、次のような 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 式の評価に時間がかかる場合があります。スクリプトによるロケーターを使用してノードを返すほうが速い可能性があります。

最終行の "Details" リンクを検索するスクリプトは次のようになります。


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" 属性があった場合、[要素のプロパティを使用] するロケーターを作成するか、//a[@id='value'] のように非常に単純な XPath を作成することができます。

  • No labels