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 }