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