View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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;
16  
17  import java.util.Arrays;
18  import java.util.HashSet;
19  import java.util.List;
20  import java.util.Set;
21  
22  import org.htmlunit.html.DomElement;
23  import org.htmlunit.html.HtmlAnchor;
24  import org.htmlunit.html.HtmlElement;
25  import org.htmlunit.html.HtmlInput;
26  import org.htmlunit.html.HtmlPage;
27  
28  /**
29   * Utility class which contains standard assertions for HTML pages.
30   *
31   * <p>This class provides a collection of static assertion methods for testing
32   * HTML page content, structure, and behavior. All assertion methods throw
33   * {@link AssertionError} when the expected condition is not met.</p>
34   *
35   * <p>Common use cases include:</p>
36   * <ul>
37   *   <li>Verifying page titles and content</li>
38   *   <li>Checking for presence/absence of elements</li>
39   *   <li>Validating form inputs and links</li>
40   *   <li>Ensuring accessibility attributes are properly set</li>
41   * </ul>
42   *
43   * @author Daniel Gredler
44   * @author Mike Bowler
45   * @author Ahmed Ashour
46   * @author Ronald Brill
47   */
48  public final class WebAssert {
49  
50      /**
51       * Private to prevent instantiation.
52       */
53      private WebAssert() {
54          // Empty.
55      }
56  
57      /**
58       * Verifies that the specified page's title equals the specified expected title.
59       *
60       * @param page the page to check
61       * @param title the expected title
62       * @throws AssertionError if the page title does not match the expected title
63       * @throws NullPointerException if page or title is null
64       */
65      public static void assertTitleEquals(final HtmlPage page, final String title) {
66          final String s = page.getTitleText();
67          if (!title.equals(s)) {
68              final String msg = "Page title '" + s + "' does not match expected title '" + title + "'.";
69              throw new AssertionError(msg);
70          }
71      }
72  
73      /**
74       * Verifies that the specified page's title contains the specified substring.
75       *
76       * @param page the page to check
77       * @param titlePortion the substring which the page title is expected to contain
78       * @throws AssertionError if the page title does not contain the substring
79       * @throws NullPointerException if page or titlePortion is null
80       */
81      public static void assertTitleContains(final HtmlPage page, final String titlePortion) {
82          final String s = page.getTitleText();
83          if (!s.contains(titlePortion)) {
84              final String msg = "Page title '" + s + "' does not contain the expected substring '" + titlePortion + "'.";
85              throw new AssertionError(msg);
86          }
87      }
88  
89      /**
90       * Verifies that the specified page's title matches the specified regular expression.
91       *
92       * @param page the page to check
93       * @param regex the regular expression that the page title is expected to match
94       * @throws AssertionError if the page title does not match the regular expression
95       * @throws NullPointerException if page or regex is null
96       */
97      public static void assertTitleMatches(final HtmlPage page, final String regex) {
98          final String s = page.getTitleText();
99          if (!s.matches(regex)) {
100             final String msg = "Page title '" + s + "' does not match the expected regular expression '" + regex + "'.";
101             throw new AssertionError(msg);
102         }
103     }
104 
105     /**
106      * Verifies that the specified page contains an element with the specified ID.
107      *
108      * @param page the page to check
109      * @param id the ID of an element expected in the page
110      * @throws AssertionError if no element with the specified ID is found
111      * @throws NullPointerException if page or id is null
112      */
113     public static void assertElementPresent(final HtmlPage page, final String id) {
114         try {
115             page.getHtmlElementById(id);
116         }
117         catch (final ElementNotFoundException e) {
118             final String msg = "Expected element with ID '" + id + "' was not found on the page.";
119             throw new AssertionError(msg, e);
120         }
121     }
122 
123     /**
124      * Verifies that the specified page contains an element matching the specified XPath expression.
125      *
126      * <p><b>Example usage:</b></p>
127      * <pre>{@code
128      * WebAssert.assertElementPresentByXPath(page, "//div[@class='error']");
129      * WebAssert.assertElementPresentByXPath(page, "//input[@type='submit' and @value='Login']");
130      * }</pre>
131      *
132      * @param page the page to check
133      * @param xpath the XPath expression which is expected to match an element in the page
134      * @throws AssertionError if no elements match the XPath expression
135      * @throws NullPointerException if page or xpath is null
136      */
137     public static void assertElementPresentByXPath(final HtmlPage page, final String xpath) {
138         final List<?> elements = page.getByXPath(xpath);
139         if (elements.isEmpty()) {
140             final String msg = "No elements found matching the XPath expression '" + xpath + "'.";
141             throw new AssertionError(msg);
142         }
143     }
144 
145     /**
146      * Verifies that the specified page does not contain an element with the specified ID.
147      *
148      * @param page the page to check
149      * @param id the ID of an element which is expected to not exist on the page
150      * @throws AssertionError if an element with the specified ID is found
151      * @throws NullPointerException if page or id is null
152      */
153     public static void assertElementNotPresent(final HtmlPage page, final String id) {
154         try {
155             page.getHtmlElementById(id);
156         }
157         catch (final ElementNotFoundException e) {
158             return;
159         }
160         final String msg = "Found unexpected element with ID '" + id + "' on the page.";
161         throw new AssertionError(msg);
162     }
163 
164     /**
165      * Verifies that the specified page does not contain an element matching the specified XPath
166      * expression.
167      *
168      * @param page the page to check
169      * @param xpath the XPath expression which is expected to not match any element in the page
170      * @throws AssertionError if any elements match the XPath expression
171      */
172     public static void assertElementNotPresentByXPath(final HtmlPage page, final String xpath) {
173         final List<?> elements = page.getByXPath(xpath);
174         if (!elements.isEmpty()) {
175             final String msg = "Found " + elements.size()
176                                     + " unexpected element(s) matching the XPath expression '"
177                                     + xpath + "'.";
178             throw new AssertionError(msg);
179         }
180     }
181 
182     /**
183      * Verifies that the specified page contains the specified text.
184      *
185      * @param page the page to check
186      * @param text the text to check for
187      * @throws AssertionError if the page does not contain the specified text
188      * @throws NullPointerException if page or text is null
189      */
190     public static void assertTextPresent(final HtmlPage page, final String text) {
191         if (!page.asNormalizedText().contains(text)) {
192             final String msg = "Expected text '" + text + "' was not found on the page.";
193             throw new AssertionError(msg);
194         }
195     }
196 
197     /**
198      * Verifies that the element on the specified page which matches the specified ID contains the
199      * specified text.
200      *
201      * @param page the page to check
202      * @param text the text to check for
203      * @param id the ID of the element which is expected to contain the specified text
204      * @throws AssertionError if the element does not contain the specified text,
205      *         or if no element with the specified ID exists
206      * @throws NullPointerException if any parameter is null
207      */
208     public static void assertTextPresentInElement(final HtmlPage page, final String text, final String id) {
209         try {
210             final HtmlElement element = page.getHtmlElementById(id);
211             if (!element.asNormalizedText().contains(text)) {
212                 final String msg = "Element with ID '" + id + "' does not contain the expected text '" + text + "'.";
213                 throw new AssertionError(msg);
214             }
215         }
216         catch (final ElementNotFoundException e) {
217             final String msg = "Cannot verify text content: element with ID '" + id + "' was not found on the page.";
218             throw new AssertionError(msg, e);
219         }
220     }
221 
222     /**
223      * Verifies that the specified page does not contain the specified text.
224      *
225      * @param page the page to check
226      * @param text the text to check for
227      * @throws AssertionError if the page contains the specified text
228      * @throws NullPointerException if page or text is null
229      */
230     public static void assertTextNotPresent(final HtmlPage page, final String text) {
231         if (page.asNormalizedText().contains(text)) {
232             final String msg = "Found unexpected text '" + text + "' on the page.";
233             throw new AssertionError(msg);
234         }
235     }
236 
237     /**
238      * Verifies that the element on the specified page which matches the specified ID does not
239      * contain the specified text.
240      *
241      * <p>If no element with the specified ID exists, an {@link AssertionError} is thrown —
242      * the element must be present for its text content to be verified.
243      * </p>
244      *
245      * @param page the page to check
246      * @param text the text to check for
247      * @param id the ID of the element which is expected to not contain the specified text
248      * @throws AssertionError if the element contains the text, or if no element with
249      *         the specified ID exists
250      * @throws NullPointerException if any parameter is null
251      */
252     public static void assertTextNotPresentInElement(final HtmlPage page, final String text, final String id) {
253         try {
254             final HtmlElement element = page.getHtmlElementById(id);
255             if (element.asNormalizedText().contains(text)) {
256                 final String msg = "Element with ID '" + id + "' contains unexpected text '" + text + "'.";
257                 throw new AssertionError(msg);
258             }
259         }
260         catch (final ElementNotFoundException e) {
261             final String msg = "Cannot verify text content: element with ID '" + id + "' was not found on the page.";
262             throw new AssertionError(msg, e);
263         }
264     }
265 
266     /**
267      * Verifies that the specified page contains a link with the specified ID.
268      *
269      * @param page the page to check
270      * @param id the ID of the link which the page is expected to contain
271      * @throws AssertionError if no link with the specified ID is found
272      * @see #assertLinkNotPresent(HtmlPage, String)
273      * @see #assertLinkPresentWithText(HtmlPage, String)
274      */
275     public static void assertLinkPresent(final HtmlPage page, final String id) {
276         try {
277             page.getDocumentElement().getOneHtmlElementByAttribute("a", DomElement.ID_ATTRIBUTE, id);
278         }
279         catch (final ElementNotFoundException e) {
280             final String msg = "Expected link with ID '" + id + "' was not found on the page.";
281             throw new AssertionError(msg, e);
282         }
283     }
284 
285     /**
286      * Verifies that the specified page does not contain a link with the specified ID.
287      *
288      * @param page the page to check
289      * @param id the ID of the link which the page is expected to not contain
290      * @throws AssertionError if a link with the specified ID is found
291      * @see #assertLinkPresent(HtmlPage, String)
292      * @see #assertLinkNotPresentWithText(HtmlPage, String)
293      */
294     public static void assertLinkNotPresent(final HtmlPage page, final String id) {
295         try {
296             page.getDocumentElement().getOneHtmlElementByAttribute("a", DomElement.ID_ATTRIBUTE, id);
297         }
298         catch (final ElementNotFoundException expected) {
299             // expected — link should not be present
300             return;
301         }
302         throw new AssertionError("Found unexpected link with ID '" + id + "' on the page.");
303     }
304 
305     /**
306      * Verifies that the specified page contains a link with the specified text. The specified text
307      * may be a substring of the entire text contained by the link.
308      *
309      * @param page the page to check
310      * @param text the text which a link in the specified page is expected to contain
311      * @throws AssertionError if no link containing the specified text is found
312      */
313     public static void assertLinkPresentWithText(final HtmlPage page, final String text) {
314         boolean found = false;
315         for (final HtmlAnchor a : page.getAnchors()) {
316             if (a.asNormalizedText().contains(text)) {
317                 found = true;
318                 break;
319             }
320         }
321         if (!found) {
322             final String msg = "Expected link containing text '" + text + "' was not found on the page.";
323             throw new AssertionError(msg);
324         }
325     }
326 
327     /**
328      * Verifies that the specified page does not contain a link with the specified text. The
329      * specified text may be a substring of the entire text contained by the link.
330      *
331      * @param page the page to check
332      * @param text the text which a link in the specified page is not expected to contain
333      * @throws AssertionError if a link containing the specified text is found
334      */
335     public static void assertLinkNotPresentWithText(final HtmlPage page, final String text) {
336         boolean found = false;
337         for (final HtmlAnchor a : page.getAnchors()) {
338             if (a.asNormalizedText().contains(text)) {
339                 found = true;
340                 break;
341             }
342         }
343         if (found) {
344             final String msg = "Found unexpected link containing text '" + text + "' on the page.";
345             throw new AssertionError(msg);
346         }
347     }
348 
349     /**
350      * Verifies that the specified page contains a form with the specified name.
351      *
352      * @param page the page to check
353      * @param name the expected name of a form on the page
354      * @throws AssertionError if no form with the specified name is found
355      * @see #assertFormNotPresent(HtmlPage, String)
356      */
357     public static void assertFormPresent(final HtmlPage page, final String name) {
358         try {
359             page.getFormByName(name);
360         }
361         catch (final ElementNotFoundException e) {
362             final String msg = "Expected form with name '" + name + "' was not found on the page.";
363             throw new AssertionError(msg, e);
364         }
365     }
366 
367     /**
368      * Verifies that the specified page does not contain a form with the specified name.
369      *
370      * @param page the page to check
371      * @param name the name of a form which should not exist on the page
372      * @throws AssertionError if a form with the specified name is found
373      * @see #assertFormPresent(HtmlPage, String)
374      */
375     public static void assertFormNotPresent(final HtmlPage page, final String name) {
376         try {
377             page.getFormByName(name);
378         }
379         catch (final ElementNotFoundException e) {
380             return;
381         }
382         final String msg = "Found unexpected form with name '" + name + "' on the page.";
383         throw new AssertionError(msg);
384     }
385 
386     /**
387      * Verifies that the specified page contains an input element with the specified name.
388      *
389      * @param page the page to check
390      * @param name the name of the input element to look for
391      * @throws AssertionError if no input element with the specified name is found
392      * @throws NullPointerException if page or name is null
393      * @see #assertInputNotPresent(HtmlPage, String)
394      * @see #assertInputContainsValue(HtmlPage, String, String)
395      */
396     public static void assertInputPresent(final HtmlPage page, final String name) {
397         final String xpath = "//input[@name='" + name + "']";
398         final List<?> list = page.getByXPath(xpath);
399         if (list.isEmpty()) {
400             throw new AssertionError("Expected input element with name '" + name + "' was not found on the page.");
401         }
402     }
403 
404     /**
405      * Verifies that the specified page does not contain an input element with the specified name.
406      *
407      * @param page the page to check
408      * @param name the name of the input element to look for
409      * @throws AssertionError if an input element with the specified name is found
410      * @throws NullPointerException if page or name is null
411      */
412     public static void assertInputNotPresent(final HtmlPage page, final String name) {
413         final String xpath = "//input[@name='" + name + "']";
414         final List<?> list = page.getByXPath(xpath);
415         if (!list.isEmpty()) {
416             throw new AssertionError("Found unexpected input element with name '" + name + "' on the page.");
417         }
418     }
419 
420     /**
421      * Verifies that the first input element with the specified name on the specified page contains
422      * the specified value.
423      *
424      * <p>If multiple {@code <input>} elements share the same name, only the first is checked.</p>
425      *
426      * @param page the page to check
427      * @param name the name of the input element to check
428      * @param value the value to check for
429      * @throws AssertionError if no input element with the specified name is found,
430      *         or if the first matching element does not contain the expected value
431      * @throws NullPointerException if any parameter is null
432      */
433     public static void assertInputContainsValue(final HtmlPage page, final String name, final String value) {
434         final String xpath = "//input[@name='" + name + "']";
435         final List<?> list = page.getByXPath(xpath);
436         if (list.isEmpty()) {
437             throw new AssertionError("Expected input element with name '" + name + "' was not found on the page.");
438         }
439         final HtmlInput input = (HtmlInput) list.get(0);
440         final String s = input.getValue();
441         if (!s.equals(value)) {
442             throw new AssertionError("Input element '" + name + "' has value '" + s
443                             + "' but expected '" + value + "'.");
444         }
445     }
446 
447     /**
448      * Verifies that the first input element with the specified name on the specified page does not
449      * contain the specified value.
450      *
451      * <p>If multiple {@code <input>} elements share the same name, only the first is checked.</p>
452      *
453      * @param page the page to check
454      * @param name the name of the input element to check
455      * @param value the value to check for
456      * @throws AssertionError if no input element with the specified name is found,
457      *         or if the first matching element contains the unexpected value
458      * @throws NullPointerException if any parameter is null
459      */
460     public static void assertInputDoesNotContainValue(final HtmlPage page, final String name, final String value) {
461         final String xpath = "//input[@name='" + name + "']";
462         final List<?> list = page.getByXPath(xpath);
463         if (list.isEmpty()) {
464             throw new AssertionError("Expected input element with name '" + name + "' was not found on the page.");
465         }
466         final HtmlInput input = (HtmlInput) list.get(0);
467         final String s = input.getValue();
468         if (s.equals(value)) {
469             throw new AssertionError("Input element '" + name + "' has unexpected value '" + s + "'.");
470         }
471     }
472 
473     /**
474      * <p>Many HTML elements are "tabbable" and can have a {@code tabindex} attribute
475      * that determines the order in which the components are navigated when
476      * pressing the tab key. To ensure good usability for keyboard navigation,
477      * all tabbable elements should have the {@code tabindex} attribute set.</p>
478      *
479      * <p>This method verifies that all tabbable elements have a valid value set for
480      * the {@code tabindex} attribute. Valid values are positive integers,
481      * 0 (for default tab order), or -1 (to exclude from tab order).</p>
482      *
483      * <p>The following elements are checked: a, area, button, input, object, select, textarea</p>
484      *
485      * @param page the page to check
486      * @throws AssertionError if any tabbable element is missing the {@code tabindex} attribute
487      *         or has an out-of-bounds value
488      */
489     public static void assertAllTabIndexAttributesSet(final HtmlPage page) {
490         final List<String> tags =
491             Arrays.asList("a", "area", "button", "input", "object", "select", "textarea");
492 
493         for (final String tag : tags) {
494             for (final HtmlElement element : page.getDocumentElement().getStaticElementsByTagName(tag)) {
495                 final Short tabIndex = element.getTabIndex();
496                 if (tabIndex == null) {
497                     throw new AssertionError("Element " + element.asXml()
498                         + " tabindex attribute is missing or not numeric.");
499                 }
500                 if (HtmlElement.TAB_INDEX_OUT_OF_BOUNDS.equals(tabIndex)) {
501                     final String s = element.getAttributeDirect("tabindex");
502                     throw new AssertionError("Element " + element.asXml()
503                         + " has an out-of-bounds tabindex value '" + s + "'.");
504                 }
505             }
506         }
507     }
508 
509     /**
510      * Many HTML components can have an {@code accesskey} attribute which defines a hot key for
511      * keyboard navigation. This method verifies that all the {@code accesskey} attributes on the
512      * specified page are unique.
513      *
514      * <p>Duplicate access keys can confuse users and make keyboard navigation unpredictable.</p>
515      *
516      * @param page the page to check
517      * @throws AssertionError if any access key is used more than once on the page
518      */
519     public static void assertAllAccessKeyAttributesUnique(final HtmlPage page) {
520         final Set<String> seen = new HashSet<>();
521         for (final HtmlElement element : page.getHtmlElementDescendants()) {
522             final String key = element.getAttributeDirect("accesskey");
523             if (key != null && !key.isEmpty()) {
524                 if (!seen.add(key)) {
525                     throw new AssertionError("Duplicate access key '" + key + "' found on the page.");
526                 }
527             }
528         }
529     }
530 
531     /**
532      * Verifies that all element IDs in the specified page are unique.
533      *
534      * @param page the page to check
535      * @throws AssertionError if any element ID is used more than once on the page
536      * @throws NullPointerException if page is null
537      */
538     public static void assertAllIdAttributesUnique(final HtmlPage page) {
539         final Set<String> seen = new HashSet<>();
540         for (final HtmlElement element : page.getHtmlElementDescendants()) {
541             final String id = element.getId();
542             if (id != null && !id.isEmpty()) {
543                 if (!seen.add(id)) {
544                     throw new AssertionError("Duplicate element ID '" + id + "' found on the page.");
545                 }
546             }
547         }
548     }
549 
550     /**
551      * Asserts that the specified object is not {@code null}, throwing a
552      * {@link NullPointerException} with the given description if it is.
553      *
554      * @param description the message for the {@link NullPointerException}
555      * @param object the object to check
556      * @throws NullPointerException if {@code object} is {@code null}
557      */
558     public static void notNull(final String description, final Object object) {
559         if (object == null) {
560             throw new NullPointerException(description);
561         }
562     }
563 }