View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html.xpath;
16  
17  import org.htmlunit.WebDriverTestCase;
18  import org.htmlunit.junit.annotation.Alerts;
19  import org.junit.jupiter.api.Test;
20  
21  /**
22   * Tests for XPath evaluation on HtmlUnit DOM.
23   *
24   * @author Marc Guillemot
25   * @author Ahmed Ashour
26   * @author Ronald Brill
27   */
28  public class HtmlUnitXPath2Test extends WebDriverTestCase {
29  
30      /**
31       * {@inheritDoc}
32       */
33      @Override
34      protected boolean isWebClientCached() {
35          return true;
36      }
37  
38      /**
39       * @throws Exception if the test fails
40       */
41      @Test
42      @Alerts({"4", "null"})
43      public void xPathNull() throws Exception {
44          final String content = DOCTYPE_HTML
45              + "<html><head>\n"
46              + "<script>\n"
47              + LOG_TITLE_FUNCTION
48              + "function test() {\n"
49              + "  try {\n"
50              + "    var node = '';"
51              + "    var expr = null;\n"
52              + "    var result = document.evaluate(expr, document.documentElement, null, XPathResult.ANY_TYPE, null);\n"
53              + "    node = result.iterateNext();\n"
54              + "    log(result.resultType);\n"
55              + "    log(node);\n"
56              + "  } catch(e) { logEx(e) }\n"
57              + "}\n"
58              + "</script></head>\n"
59              + "<body onload='test()'>\n"
60              + "  <select name='test'><option value='1'>foo&nbsp;and&nbsp;foo</option></select>\n"
61              + "</body></html>";
62  
63          loadPageVerifyTitle2(content);
64      }
65  
66      /**
67       * @throws Exception if the test fails
68       */
69      @Test
70      @Alerts({"4", "null"})
71      public void xPathUndefined() throws Exception {
72          final String content = DOCTYPE_HTML
73              + "<html><head>\n"
74              + "<script>\n"
75              + LOG_TITLE_FUNCTION
76              + "function test() {\n"
77              + "  try {\n"
78              + "    var node = '';"
79              + "    var expr = undefined;\n"
80              + "    var result = document.evaluate(expr, document.documentElement, null, XPathResult.ANY_TYPE, null);\n"
81              + "    node = result.iterateNext();\n"
82              + "    log(result.resultType);\n"
83              + "    log(node);\n"
84              + "  } catch(e) { logEx(e) }\n"
85              + "}\n"
86              + "</script></head>\n"
87              + "<body onload='test()'>\n"
88              + "  <select name='test'><option value='1'>foo&nbsp;and&nbsp;foo</option></select>\n"
89              + "</body></html>";
90  
91          loadPageVerifyTitle2(content);
92      }
93  
94      /**
95       * @throws Exception if the test fails
96       */
97      @Test
98      @Alerts({"102", "111", "111", "160", "97", "110", "100", "160", "102", "111", "111"})
99      public void optionText() throws Exception {
100         final String content = DOCTYPE_HTML
101             + "<html><head>\n"
102             + "<script>\n"
103             + LOG_TITLE_FUNCTION
104             + "function test() {\n"
105             + "  try {\n"
106             + "    var expr = 'string(//option)';\n"
107             + "    var result = document.evaluate(expr, document.documentElement, null, XPathResult.ANY_TYPE, null);\n"
108             + "    var value = result.stringValue;\n"
109             + "    for (var i = 0; i < value.length; i++) {\n"
110             + "      log(value.charCodeAt(i));\n"
111             + "    }\n"
112             + "  } catch(e) { logEx(e) }\n"
113             + "}\n"
114             + "</script></head>\n"
115             + "<body onload='test()'>\n"
116             + "  <select name='test'><option value='1'>foo&nbsp;and&nbsp;foo</option></select>\n"
117             + "</body></html>";
118 
119         loadPageVerifyTitle2(content);
120     }
121 
122     /**
123      * @throws Exception if the test fails
124      */
125     @Test
126     @Alerts("[object HTMLParagraphElement][object HTMLDivElement]")
127     public void pipe() throws Exception {
128         final String content = DOCTYPE_HTML
129             + "<html>\n"
130             + "<head>\n"
131             + "<script>\n"
132             + LOG_TITLE_FUNCTION
133             + "function test() {\n"
134             + "  try {\n"
135             + "    var res = '';"
136             + "    var expr = '//p | //div';\n"
137             + "    var result = document.evaluate(expr, document.documentElement, null, XPathResult.ANY_TYPE, null);\n"
138             + "    while (node = result.iterateNext()) {\n"
139             + "      res += node;\n"
140             + "    }\n"
141             + "    log(res);\n"
142             + "  } catch(e) { logEx(e) }\n"
143             + "}\n"
144             + "</script></head>\n"
145             + "<body onload='test()'>\n"
146             + "  <p></p>\n"
147             + "  <div></div>\n"
148             + "</body></html>";
149 
150         loadPageVerifyTitle2(content);
151     }
152 
153     /**
154      * @throws Exception if the test fails
155      */
156     @Test
157     @Alerts("a")
158     public void math() throws Exception {
159         final String content = DOCTYPE_HTML
160             + "<html>\n"
161             + "<head>\n"
162             + "<script>\n"
163             + LOG_TITLE_FUNCTION
164             + "function test() {\n"
165             + "  try {\n"
166             + "    var res = '';"
167             + "    var expr = '//p[position()=(1+5-(2*2))div 2]';\n"
168             + "    var result = document.evaluate(expr, document.documentElement, null, XPathResult.ANY_TYPE, null);\n"
169             + "    while (node = result.iterateNext()) {\n"
170             + "      res += node.id;\n"
171             + "    }\n"
172             + "    log(res);\n"
173             + "  } catch(e) { logEx(e) }\n"
174             + "}\n"
175             + "</script></head>\n"
176             + "<body onload='test()'>\n"
177             + "  <div>\n"
178             + "    <p id='a'></p>\n"
179             + "    <p id='b'></p>\n"
180             + "  </div>\n"
181             + "</body></html>";
182 
183         loadPageVerifyTitle2(content);
184     }
185 
186     /**
187      * @throws Exception if the test fails
188      */
189     @Test
190     @Alerts("b")
191     public void gt() throws Exception {
192         compare("//p[position()>1]");
193     }
194 
195     /**
196      * @throws Exception if the test fails
197      */
198     @Test
199     @Alerts("ab")
200     public void gte() throws Exception {
201         compare("//p[position()>=1]");
202     }
203 
204     /**
205      * @throws Exception if the test fails
206      */
207     @Test
208     @Alerts("a")
209     public void lt() throws Exception {
210         compare("//p[position()<2]");
211     }
212 
213     /**
214      * @throws Exception if the test fails
215      */
216     @Test
217     @Alerts("ab")
218     public void lte() throws Exception {
219         compare("//p[position()<=2]");
220     }
221 
222     /**
223      * @throws Exception if the test fails
224      */
225     @Test
226     @Alerts("b")
227     public void eq() throws Exception {
228         compare("//p[position()=2]");
229     }
230 
231     /**
232      * @throws Exception if the test fails
233      */
234     @Test
235     @Alerts("b")
236     public void neq() throws Exception {
237         compare("//p[position()!=1]");
238     }
239 
240     /**
241      * @throws Exception if the test fails
242      */
243     @Test
244     @Alerts("a")
245     public void and() throws Exception {
246         compare("//p[@x>= 0 and @y=7]");
247     }
248 
249     /**
250      * @throws Exception if the test fails
251      */
252     @Test
253     @Alerts("ab")
254     public void or() throws Exception {
255         compare("//p[@x>= 0 or @y>4]");
256     }
257 
258     /**
259      * @throws Exception if the test fails
260      */
261     @Test
262     @Alerts("ab")
263     public void mod() throws Exception {
264         compare("//p[@y mod 6 = 1]");
265     }
266 
267     /**
268      * @throws Exception if the test fails
269      */
270     @Test
271     @Alerts("'adc'")
272     public void translate() throws Exception {
273         compareStringValue("translate(\"abc\", \"b\", \"d\")");
274     }
275 
276     /**
277      * @throws Exception if the test fails
278      */
279     @Test
280     @Alerts("true")
281     public void trueTest() throws Exception {
282         compareBooleanValue("true()");
283     }
284 
285     /**
286      * @throws Exception if the test fails
287      */
288     @Test
289     @Alerts("false")
290     public void falseTest() throws Exception {
291         compareBooleanValue("false()");
292     }
293 
294     /**
295      * @throws Exception if the test fails
296      */
297     @Test
298     @Alerts("true")
299     public void booleanTest() throws Exception {
300         compareBooleanValue("boolean(7)");
301     }
302 
303     /**
304      * @throws Exception if the test fails
305      */
306     @Test
307     @Alerts("false")
308     public void booleanTestFalse() throws Exception {
309         compareBooleanValue("boolean(0)");
310     }
311 
312     /**
313      * @throws Exception if the test fails
314      */
315     @Test
316     @Alerts("b")
317     public void number() throws Exception {
318         compare("//p[@y=number(\"  13\t \")]");
319     }
320 
321     /**
322      * @throws Exception if the test fails
323      */
324     @Test
325     @Alerts("false")
326     public void startsWith() throws Exception {
327         compareBooleanValue("starts-with(\"haystack\", \"needle\")");
328     }
329 
330     /**
331      * @throws Exception if the test fails
332      */
333     @Test
334     @Alerts("true")
335     public void startsWithFound() throws Exception {
336         compareBooleanValue("starts-with(\"haystack\", \"hay\")");
337     }
338 
339     /**
340      * @throws Exception if the test fails
341      */
342     @Test
343     @Alerts("true")
344     public void startsWithWhole() throws Exception {
345         compareBooleanValue("starts-with(\"haystack\", \"haystack\")");
346     }
347 
348     /**
349      * @throws Exception if the test fails
350      */
351     @Test
352     @Alerts("true")
353     public void startsWithEmpty() throws Exception {
354         compareBooleanValue("starts-with(\"haystack\", \"\")");
355     }
356 
357     /**
358      * @throws Exception if the test fails
359      */
360     @Test
361     @Alerts("true")
362     public void startsWithEmptyEmpty() throws Exception {
363         compareBooleanValue("starts-with(\"\", \"\")");
364     }
365 
366     /**
367      * @throws Exception if the test fails
368      */
369     @Test
370     @Alerts("'tml'")
371     public void substring() throws Exception {
372         compareStringValue("substring(\"HtmlUnit\", 2, 3)");
373     }
374 
375     /**
376      * @throws Exception if the test fails
377      */
378     @Test
379     @Alerts("''")
380     public void substringWithNegativeLength() throws Exception {
381         compareStringValue("substring(\"HtmlUnit\", 2, -1)");
382     }
383 
384     /**
385      * @throws Exception in case of problems
386      */
387     @Test
388     @Alerts("''")
389     public void substringNegativeStartWithLength() throws Exception {
390         compareStringValue("substring(\"HtmlUnit\", -50, 20)");
391     }
392 
393     /**
394      * @throws Exception if the test fails
395      */
396     @Test
397     @Alerts("'Unit'")
398     public void substringAfter() throws Exception {
399         compareStringValue("substring-after(\"HtmlUnit\", \"tml\")");
400     }
401 
402     /**
403      * @throws Exception if the test fails
404      */
405     @Test
406     @Alerts("'Html'")
407     public void substringBefore() throws Exception {
408         compareStringValue("substring-before(\"HtmlUnit\", \"Uni\")");
409     }
410 
411     /**
412      * @throws Exception if the test fails
413      */
414     @Test
415     @Alerts("false")
416     public void not() throws Exception {
417         compareBooleanValue("not(\"HtmlUnit\")");
418     }
419 
420     /**
421      * @throws Exception if the test fails
422      */
423     @Test
424     @Alerts("a")
425     public void attrib() throws Exception {
426         compare("//p[@x=1]");
427     }
428 
429     /**
430      * @throws Exception if the test fails
431      */
432     @Test
433     @Alerts("SyntaxError/DOMException")
434     public void lowerCaseNotSupported() throws Exception {
435         compareError("//*[lower-case(@id) = \"a\"]");
436     }
437 
438     /**
439      * @throws Exception if the test fails
440      */
441     @Test
442     @Alerts("SyntaxError/DOMException")
443     public void upperCaseNotSupported() throws Exception {
444         compareError("//*[upper-case(@id) = \"A\"]");
445     }
446 
447     /**
448      * @throws Exception if the test fails
449      */
450     @Test
451     @Alerts("SyntaxError/DOMException")
452     public void endsWithNotSupported() throws Exception {
453         compareError("ends-with(\"haystack\", \"haystack\")");
454     }
455 
456     private void compare(final String xpath) throws Exception {
457         final String content = DOCTYPE_HTML
458             + "<html>\n"
459             + "<head>\n"
460             + "<script>\n"
461             + LOG_TITLE_FUNCTION
462             + "function test() {\n"
463             + "  try {\n"
464             + "    var res = '';\n"
465             + "    var expr = '" + xpath + "';\n"
466             + "    var result = document.evaluate(expr, document.documentElement, null, XPathResult.ANY_TYPE, null);\n"
467             + "    while (node = result.iterateNext()) {\n"
468             + "      res += node.id;\n"
469             + "    }\n"
470             + "    log(res);\n"
471             + "  } catch(e) { logEx(e) }\n"
472             + "}\n"
473             + "</script></head>\n"
474             + "<body onload='test()'>\n"
475             + "  <div>\n"
476             + "    <p id='a' x='1' y='7'></p>\n"
477             + "    <p id='b' y='13'></p>\n"
478             + "  </div>\n"
479             + "</body></html>";
480 
481         loadPageVerifyTitle2(content);
482     }
483 
484     private void compareStringValue(final String xpath) throws Exception {
485         final String content = DOCTYPE_HTML
486             + "<html>\n"
487             + "<head>\n"
488             + "<script>\n"
489             + LOG_TITLE_FUNCTION
490             + "function test() {\n"
491             + "  try {\n"
492             + "    var res = '';\n"
493             + "    var expr = '" + xpath + "';\n"
494             + "    var result = document.evaluate(expr, document.documentElement, null, XPathResult.ANY_TYPE, null);\n"
495             + "    log(\"'\" + result.stringValue + \"'\");\n"
496             + "  } catch(e) { logEx(e) }\n"
497             + "}\n"
498             + "</script></head>\n"
499             + "<body onload='test()'>\n"
500             + "  <div>\n"
501             + "    <p id='a' x='1' y='7'></p>\n"
502             + "    <p id='b' y='13'></p>\n"
503             + "  </div>\n"
504             + "</body></html>";
505 
506         loadPageVerifyTitle2(content);
507     }
508 
509     private void compareBooleanValue(final String xpath) throws Exception {
510         final String content = DOCTYPE_HTML
511             + "<html>\n"
512             + "<head>\n"
513             + "<script>\n"
514             + LOG_TITLE_FUNCTION
515             + "function test() {\n"
516             + "  try {\n"
517             + "    var res = '';\n"
518             + "    var expr = '" + xpath + "';\n"
519             + "    var result = document.evaluate(expr, document.documentElement, null, XPathResult.ANY_TYPE, null);\n"
520             + "    log(result.booleanValue);\n"
521             + "  } catch(e) { logEx(e) }\n"
522             + "}\n"
523             + "</script></head>\n"
524             + "<body onload='test()'>\n"
525             + "  <div>\n"
526             + "    <p id='a' x='1' y='7'></p>\n"
527             + "    <p id='b' y='13'></p>\n"
528             + "  </div>\n"
529             + "</body></html>";
530 
531         loadPageVerifyTitle2(content);
532     }
533 
534     private void compareError(final String xpath) throws Exception {
535         final String content = DOCTYPE_HTML
536             + "<html>\n"
537             + "<head>\n"
538             + "<script>\n"
539             + LOG_TITLE_FUNCTION
540             + "function test() {\n"
541             + "  try {\n"
542             + "    var expr = '" + xpath + "';\n"
543             + "    var result = document.evaluate(expr, document.documentElement, null, XPathResult.ANY_TYPE, null);\n"
544             + "    log('error expected');\n"
545             + "  } catch(e) { logEx(e) }\n"
546             + "}\n"
547             + "</script></head>\n"
548             + "<body onload='test()'>\n"
549             + "</body></html>";
550 
551         loadPageVerifyTitle2(content);
552     }
553 
554     /**
555      * @throws Exception if the test fails
556      */
557     @Test
558     @Alerts("mySpan")
559     public void minimalParameters() throws Exception {
560         final String content = DOCTYPE_HTML
561             + "<html>\n"
562             + "<head>\n"
563             + "<script>\n"
564             + LOG_TITLE_FUNCTION
565             + "function test() {\n"
566             + "  try {\n"
567             + "    var res = '';\n"
568             + "    var result = document.evaluate('//span', document.documentElement);\n"
569             + "    while (node = result.iterateNext()) {\n"
570             + "      res += node.id;\n"
571             + "    }\n"
572             + "    log(res);\n"
573             + "  } catch(e) { logEx(e) }\n"
574             + "}\n"
575             + "</script></head>\n"
576             + "<body onload='test()'>\n"
577             + "  <div id='myDiv' attr='false'>false</span>\n"
578             + "  <span id='mySpan' attr='true'>true</span>\n"
579             + "</body></html>";
580 
581         loadPageVerifyTitle2(content);
582     }
583 
584     /**
585      * @throws Exception if the test fails
586      */
587     @Test
588     @Alerts("mySpan")
589     public void undefinedResult() throws Exception {
590         final String content = DOCTYPE_HTML
591             + "<html>\n"
592             + "<head>\n"
593             + "<script>\n"
594             + LOG_TITLE_FUNCTION
595             + "function test() {\n"
596             + "  try {\n"
597             + "    var res = '';\n"
598             + "    var result = document.evaluate('//span', "
599                         + "document.documentElement, null, XPathResult.ANY_TYPE, undefined);\n"
600             + "    while (node = result.iterateNext()) {\n"
601             + "      res += node.id;\n"
602             + "    }\n"
603             + "    log(res);\n"
604             + "  } catch(e) { logEx(e) }\n"
605             + "}\n"
606             + "</script></head>\n"
607             + "<body onload='test()'>\n"
608             + "  <div id='myDiv' attr='false'>false</span>\n"
609             + "  <span id='mySpan' attr='true'>true</span>\n"
610             + "</body></html>";
611 
612         loadPageVerifyTitle2(content);
613     }
614 
615     /**
616      * @throws Exception if the test fails
617      */
618     @Test
619     @Alerts("TypeError")
620     public void stringResult() throws Exception {
621         final String content = DOCTYPE_HTML
622             + "<html>\n"
623             + "<head>\n"
624             + "<script>\n"
625             + LOG_TITLE_FUNCTION
626             + "function test() {\n"
627             + "  try {\n"
628             + "    var res = '';\n"
629             + "    var result = document.evaluate('//span', "
630                         + "document.documentElement, null, XPathResult.ANY_TYPE, 'abcd');\n"
631             + "    while (node = result.iterateNext()) {\n"
632             + "      res += node.id;\n"
633             + "    }\n"
634             + "    log(res);\n"
635             + "  } catch(e) { logEx(e) }\n"
636             + "}\n"
637             + "</script></head>\n"
638             + "<body onload='test()'>\n"
639             + "  <div id='myDiv' attr='false'>false</span>\n"
640             + "  <span id='mySpan' attr='true'>true</span>\n"
641             + "</body></html>";
642 
643         loadPageVerifyTitle2(content);
644     }
645 
646     /**
647      * @throws Exception if the test fails
648      */
649     @Test
650     @Alerts("mySpan")
651     public void objectResult() throws Exception {
652         final String content = DOCTYPE_HTML
653             + "<html>\n"
654             + "<head>\n"
655             + "<script>\n"
656             + LOG_TITLE_FUNCTION
657             + "function test() {\n"
658             + "  try {\n"
659             + "    var res = '';\n"
660             + "    var result = document.evaluate('//span', "
661                         + "document.documentElement, null, XPathResult.ANY_TYPE, {});\n"
662             + "    while (node = result.iterateNext()) {\n"
663             + "      res += node.id;\n"
664             + "    }\n"
665             + "    log(res);\n"
666             + "  } catch(e) { logEx(e) }\n"
667             + "}\n"
668             + "</script></head>\n"
669             + "<body onload='test()'>\n"
670             + "  <div id='myDiv' attr='false'>false</span>\n"
671             + "  <span id='mySpan' attr='true'>true</span>\n"
672             + "</body></html>";
673 
674         loadPageVerifyTitle2(content);
675     }
676 
677     /**
678      * @throws Exception if the test fails
679      */
680     @Test
681     @Alerts(DEFAULT = "mySpan - - myDiv",
682             FF = "mySpan - myDiv - ",
683             FF_ESR = "mySpan - myDiv - ")
684     public void reuseResult() throws Exception {
685         final String content = DOCTYPE_HTML
686             + "<html>\n"
687             + "<head>\n"
688             + "<script>\n"
689             + LOG_TITLE_FUNCTION
690             + "function test() {\n"
691             + "  try {\n"
692             + "    var res = '';\n"
693             + "    var result = document.evaluate('//span', "
694                         + "document.documentElement, null, XPathResult.ANY_TYPE);\n"
695             + "    while (node = result.iterateNext()) {\n"
696             + "      res += node.id;\n"
697             + "    }\n"
698             + "    res += ' - ';\n"
699 
700             + "    var result2 = document.evaluate('//div', "
701                         + "document.documentElement, null, XPathResult.ANY_TYPE, result);\n"
702             + "    while (node = result.iterateNext()) {\n"
703             + "      res += node.id;\n"
704             + "    }\n"
705             + "    res += ' - ';\n"
706 
707             + "    while (node = result2.iterateNext()) {\n"
708             + "      res += node.id;\n"
709             + "    }\n"
710 
711             + "    log(res);\n"
712             + "  } catch(e) { logEx(e) }\n"
713             + "}\n"
714             + "</script></head>\n"
715             + "<body onload='test()'>\n"
716             + "  <div id='myDiv' attr='false'>false</span>\n"
717             + "  <span id='mySpan' attr='true'>true</span>\n"
718             + "</body></html>";
719 
720         loadPageVerifyTitle2(content);
721     }
722 
723     /**
724      * @throws Exception if the test fails
725      */
726     @Test
727     @Alerts("myDiv1")
728     public void documentEvaluateFirst() throws Exception {
729         final String content = DOCTYPE_HTML
730             + "<html>\n"
731             + "<head>\n"
732             + "<script>\n"
733             + LOG_TITLE_FUNCTION
734             + "function test() {\n"
735             + "  try {\n"
736             + "    var res = '';\n"
737             + "    var result = document.evaluate('//div', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE);\n"
738             + "    log(result.singleNodeValue.id);\n"
739             + "  } catch(e) { logEx(e) }\n"
740             + "}\n"
741             + "</script></head>\n"
742             + "<body onload='test()'>\n"
743             + "  <div id='myDiv1'>div1</div>\n"
744             + "  <div id='myDiv2'>div1</div>\n"
745             + "</body></html>";
746 
747         loadPageVerifyTitle2(content);
748     }
749 }