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 static java.nio.charset.StandardCharsets.ISO_8859_1;
18  import static org.junit.jupiter.api.Assertions.fail;
19  
20  import java.awt.image.BufferedImage;
21  import java.io.ByteArrayInputStream;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.Serializable;
26  import java.net.MalformedURLException;
27  import java.net.URL;
28  import java.nio.file.Files;
29  import java.nio.file.Paths;
30  import java.time.Duration;
31  import java.util.ArrayList;
32  import java.util.Arrays;
33  import java.util.Base64;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Objects;
37  import java.util.function.Supplier;
38  
39  import javax.imageio.ImageIO;
40  
41  import org.apache.commons.io.IOUtils;
42  import org.apache.commons.lang3.SerializationUtils;
43  import org.htmlunit.junit.BrowserVersionClassTemplateInvocationContextProvider;
44  import org.htmlunit.junit.ErrorOutputChecker;
45  import org.htmlunit.junit.SetExpectedAlertsBeforeTestExecutionCallback;
46  import org.junit.jupiter.api.AfterAll;
47  import org.junit.jupiter.api.AfterEach;
48  import org.junit.jupiter.api.Assertions;
49  import org.junit.jupiter.api.BeforeAll;
50  import org.junit.jupiter.api.BeforeEach;
51  import org.junit.jupiter.api.ClassTemplate;
52  import org.junit.jupiter.api.MethodOrderer;
53  import org.junit.jupiter.api.TestInfo;
54  import org.junit.jupiter.api.TestMethodOrder;
55  import org.junit.jupiter.api.extension.ExtendWith;
56  
57  import com.github.romankh3.image.comparison.ImageComparison;
58  import com.github.romankh3.image.comparison.ImageComparisonUtil;
59  import com.github.romankh3.image.comparison.model.ImageComparisonResult;
60  import com.github.romankh3.image.comparison.model.ImageComparisonState;
61  
62  /**
63   * Common superclass for HtmlUnit tests.
64   *
65   * @author Mike Bowler
66   * @author David D. Kilzer
67   * @author Marc Guillemot
68   * @author Chris Erskine
69   * @author Michael Ottati
70   * @author Daniel Gredler
71   * @author Ahmed Ashour
72   * @author Ronald Brill
73   */
74  @ClassTemplate
75  @ExtendWith({BrowserVersionClassTemplateInvocationContextProvider.class,
76               SetExpectedAlertsBeforeTestExecutionCallback.class,
77               ErrorOutputChecker.class})
78  @TestMethodOrder(MethodOrderer.DisplayName.class)
79  public abstract class WebTestCase {
80  
81      /** The html5 doctype. */
82      public static final String DOCTYPE_HTML = "<!DOCTYPE html>\n";
83  
84      /**
85       * Make the test method name available to the tests.
86       */
87      public TestInfo testInfo_;
88  
89      // /** Logging support. */
90      // private static final Log LOG = LogFactory.getLog(WebTestCase.class);
91  
92      /** save the environment */
93      private static final Locale SAVE_LOCALE = Locale.getDefault();
94  
95      // 12346 seems to be in use on our CI server
96  
97      /** The listener port for the web server. */
98      public static final int PORT = Integer.parseInt(System.getProperty("htmlunit.test.port", "22222"));
99  
100     /** The second listener port for the web server, used for cross-origin tests. */
101     public static final int PORT2 = Integer.parseInt(System.getProperty("htmlunit.test.port2", "22223"));
102 
103     /** The third listener port for the web server, used for cross-origin tests. */
104     public static final int PORT3 = Integer.parseInt(System.getProperty("htmlunit.test.port3", "22224"));
105 
106     /** The listener port used for our primitive server tests. */
107     public static final int PORT_PRIMITIVE_SERVER = Integer.parseInt(
108                                                         System.getProperty("htmlunit.test.port_primitive", "22225"));
109 
110     /** The listener port used for our proxy tests. */
111     public static final int PORT_PROXY_SERVER = Integer.parseInt(
112                                                         System.getProperty("htmlunit.test.port_proxy", "22226"));
113 
114     /** The SOCKS proxy port to use for SOCKS proxy tests. */
115     public static final int SOCKS_PROXY_PORT = Integer.parseInt(
116             System.getProperty("htmlunit.test.socksproxy.port", "22227"));
117 
118     /** The SOCKS proxy host to use for SOCKS proxy tests. */
119     public static final String SOCKS_PROXY_HOST = System.getProperty("htmlunit.test.socksproxy.host", "localhost");
120 
121     /** The default time used to wait for the expected alerts. */
122     protected static final Duration DEFAULT_WAIT_TIME = Duration.ofSeconds(1);
123 
124     /** Constant for the URL which is used in the tests. */
125     public static final URL URL_FIRST;
126 
127     /** Constant for the URL which is used in the tests. */
128     public static final URL URL_SECOND;
129 
130     /**
131      * Constant for the URL which is used in the tests.
132      * This URL doesn't use the same host name as {@link #URL_FIRST} and {@link #URL_SECOND}.
133      */
134     public static final URL URL_THIRD;
135 
136     /**
137      * Constant for a URL used in tests that responds with Access-Control-Allow-Origin.
138      */
139     public static final URL URL_CROSS_ORIGIN;
140 
141     /**
142      * To get an origin header with two things in it, there needs to be a chain of two
143      * cross-origin referers. So we need a second extra origin.
144      */
145     public static final URL URL_CROSS_ORIGIN2;
146 
147     /**
148      * Constant for the base URL for cross-origin tests.
149      */
150     public static final URL URL_CROSS_ORIGIN_BASE;
151 
152     private BrowserVersion browserVersion_;
153     private String[] expectedAlerts_;
154     private MockWebConnection mockWebConnection_;
155 
156     static {
157         try {
158             URL_FIRST = new URL("http://localhost:" + PORT + "/");
159             URL_SECOND = new URL("http://localhost:" + PORT + "/second/");
160             URL_THIRD = new URL("http://127.0.0.1:" + PORT + "/third/");
161             URL_CROSS_ORIGIN = new URL("http://127.0.0.1:" + PORT2 + "/corsAllowAll");
162             URL_CROSS_ORIGIN2 = new URL("http://localhost:" + PORT3 + "/");
163             URL_CROSS_ORIGIN_BASE = new URL("http://localhost:" + PORT2 + "/");
164         }
165         catch (final MalformedURLException e) {
166             // This is theoretically impossible.
167             throw new IllegalStateException("Unable to create URL constants");
168         }
169     }
170 
171     /**
172      * Constructor.
173      */
174     protected WebTestCase() {
175     }
176 
177     @BeforeEach
178     void init(final TestInfo testInfo) {
179         testInfo_ = testInfo;
180     }
181 
182     /**
183      * Assert that the specified object is null.
184      * @param object the object to check
185      */
186     public static void assertNull(final Object object) {
187         Assertions.assertNull(object, "Expected null but found [" + object + "]");
188     }
189 
190     /**
191      * Assert that the specified object is null.
192      * @param message the message
193      * @param object the object to check
194      */
195     public static void assertNull(final String message, final Object object) {
196         Assertions.assertNull(object, message);
197     }
198 
199     /**
200      * Assert that the specified object is not null.
201      * @param object the object to check
202      */
203     public static void assertNotNull(final Object object) {
204         Assertions.assertNotNull(object);
205     }
206 
207     /**
208      * Assert that the specified object is not null.
209      * @param message the message
210      * @param object the object to check
211      */
212     public static void assertNotNull(final String message, final Object object) {
213         Assertions.assertNotNull(object, message);
214     }
215 
216     /**
217      * Asserts that two objects refer to the same object.
218      * @param expected the expected object
219      * @param actual the actual object
220      */
221     public static void assertSame(final Object expected, final Object actual) {
222         Assertions.assertSame(expected, actual);
223     }
224 
225     /**
226      * Asserts that two objects refer to the same object.
227      * @param message the message
228      * @param expected the expected object
229      * @param actual the actual object
230      */
231     public static void assertSame(final String message, final Object expected, final Object actual) {
232         Assertions.assertSame(expected, actual, message);
233     }
234 
235     /**
236      * Asserts that two objects do not refer to the same object.
237      * @param expected the expected object
238      * @param actual the actual object
239      */
240     public static void assertNotSame(final Object expected, final Object actual) {
241         Assertions.assertNotSame(expected, actual);
242     }
243 
244     /**
245      * Asserts that two objects do not refer to the same object.
246      * @param message the message
247      * @param expected the expected object
248      * @param actual the actual object
249      */
250     public static void assertNotSame(final String message, final Object expected, final Object actual) {
251         Assertions.assertNotSame(expected, actual, message);
252     }
253 
254     /**
255      * Facility to test external form of urls. Comparing external form of URLs is
256      * really faster than URL.equals() as the host doesn't need to be resolved.
257      * @param expectedUrl the expected URL
258      * @param actualUrl the URL to test
259      */
260     protected static void assertEquals(final URL expectedUrl, final URL actualUrl) {
261         Assertions.assertEquals(expectedUrl.toExternalForm(), actualUrl.toExternalForm());
262     }
263 
264     /**
265      * Asserts the two objects are equal.
266      * @param expected the expected object
267      * @param actual the object to test
268      */
269     protected static void assertEquals(final Object expected, final Object actual) {
270         Assertions.assertEquals(expected, actual);
271     }
272 
273     /**
274      * Asserts the two objects are equal.
275      * @param message the message
276      * @param expected the expected object
277      * @param actual the object to test
278      */
279     protected static void assertEquals(final String message, final Object expected, final Object actual) {
280         Assertions.assertEquals(expected, actual, message);
281     }
282 
283     /**
284      * Asserts the two ints are equal.
285      * @param expected the expected int
286      * @param actual the int to test
287      */
288     protected static void assertEquals(final int expected, final int actual) {
289         Assertions.assertEquals(expected, actual);
290     }
291 
292     /**
293      * Asserts the two boolean are equal.
294      * @param expected the expected boolean
295      * @param actual the boolean to test
296      */
297     protected void assertEquals(final boolean expected, final boolean actual) {
298         Assertions.assertEquals(Boolean.valueOf(expected), Boolean.valueOf(actual));
299     }
300 
301     /**
302      * Facility to test external form of urls. Comparing external form of URLs is
303      * really faster than URL.equals() as the host doesn't need to be resolved.
304      * @param message the message to display if assertion fails
305      * @param expectedUrl the string representation of the expected URL
306      * @param actualUrl the URL to test
307      */
308     protected void assertEquals(final String message, final URL expectedUrl, final URL actualUrl) {
309         Assertions.assertEquals(expectedUrl.toExternalForm(), actualUrl.toExternalForm(), message);
310     }
311 
312     /**
313      * Facility to test external form of a URL.
314      * @param expectedUrl the string representation of the expected URL
315      * @param actualUrl the URL to test
316      */
317     protected void assertEquals(final String expectedUrl, final URL actualUrl) {
318         Assertions.assertEquals(expectedUrl, actualUrl.toExternalForm());
319     }
320 
321     /**
322      * Facility method to avoid having to create explicitly a list from
323      * a String[] (for example when testing received alerts).
324      * Transforms the String[] to a List before calling
325      * {@link org.junit.Assert#assertEquals(java.lang.Object, java.lang.Object)}.
326      * @param expected the expected strings
327      * @param actual the collection of strings to test
328      */
329     protected void assertEquals(final String[] expected, final List<String> actual) {
330         assertEquals(null, expected, actual);
331     }
332 
333     /**
334      * Facility method to avoid having to create explicitly a list from
335      * a String[] (for example when testing received alerts).
336      * Transforms the String[] to a List before calling
337      * {@link org.junit.Assert#assertEquals(java.lang.String, java.lang.Object, java.lang.Object)}.
338      * @param message the message to display if assertion fails
339      * @param expected the expected strings
340      * @param actual the collection of strings to test
341      */
342     protected void assertEquals(final String message, final String[] expected, final List<String> actual) {
343         Assertions.assertEquals(Arrays.asList(expected).toString(), actual.toString(), message);
344     }
345 
346     /**
347      * Facility to test external form of a URL.
348      * @param message the message to display if assertion fails
349      * @param expectedUrl the string representation of the expected URL
350      * @param actualUrl the URL to test
351      */
352     protected void assertEquals(final String message, final String expectedUrl, final URL actualUrl) {
353         Assertions.assertEquals(expectedUrl, actualUrl.toExternalForm(), message);
354     }
355 
356     /**
357      * Assert the specified condition is true.
358      * @param condition condition to test
359      */
360     protected void assertTrue(final boolean condition) {
361         Assertions.assertTrue(condition);
362     }
363 
364     /**
365      * Assert the specified condition is true.
366      * @param message message to show
367      * @param condition condition to test
368      */
369     protected void assertTrue(final String message, final boolean condition) {
370         Assertions.assertTrue(condition, message);
371     }
372 
373     /**
374      * Assert the specified condition is false.
375      * @param condition condition to test
376      */
377     protected void assertFalse(final boolean condition) {
378         Assertions.assertFalse(condition);
379     }
380 
381     /**
382      * Assert the specified condition is false.
383      * @param message message to show
384      * @param condition condition to test
385      */
386     protected void assertFalse(final String message, final boolean condition) {
387         Assertions.assertFalse(condition, message);
388     }
389 
390     /**
391      * Sets the browser version.
392      * @param browserVersion the browser version
393      */
394     public void setBrowserVersion(final BrowserVersion browserVersion) {
395         browserVersion_ = browserVersion;
396     }
397 
398     /**
399      * Returns the current {@link BrowserVersion}.
400      * @return current {@link BrowserVersion}
401      */
402     public final BrowserVersion getBrowserVersion() {
403         if (browserVersion_ == null) {
404             throw new IllegalStateException("You must annotate the test class with '@RunWith(BrowserRunner.class)'");
405         }
406         return browserVersion_;
407     }
408 
409     /**
410      * Sets the expected alerts.
411      * @param expectedAlerts the expected alerts
412      */
413     public void setExpectedAlerts(final String... expectedAlerts) {
414         expectedAlerts_ = expectedAlerts;
415     }
416 
417     /**
418      * Returns the expected alerts.
419      * @return the expected alerts
420      */
421     protected String[] getExpectedAlerts() {
422         return expectedAlerts_;
423     }
424 
425     /**
426      * Expand "§§URL§§" to the provided URL in the expected alerts.
427      * @param url the URL to expand
428      */
429     protected void expandExpectedAlertsVariables(final URL url) {
430         expandExpectedAlertsVariables(url.toExternalForm());
431     }
432 
433     /**
434      * Expand "§§URL§§" to the provided URL in the expected alerts.
435      * @param url the URL to expand
436      */
437     protected void expandExpectedAlertsVariables(final String url) {
438         if (expectedAlerts_ == null) {
439             throw new IllegalStateException("You must annotate the test class with '@RunWith(BrowserRunner.class)'");
440         }
441         for (int i = 0; i < expectedAlerts_.length; i++) {
442             expectedAlerts_[i] = expectedAlerts_[i].replaceAll("§§URL§§", url);
443         }
444     }
445 
446     /**
447      * A generics-friendly version of {@link SerializationUtils#clone(Serializable)}.
448      * @param <T> the type of the object being cloned
449      * @param object the object being cloned
450      * @return a clone of the specified object
451      */
452     protected <T extends Serializable> T clone(final T object) {
453         return SerializationUtils.clone(object);
454     }
455 
456     /**
457      * Prepare the environment.
458      * Rhino has localized error message... for instance for French
459      */
460     @BeforeAll
461     public static void beforeClass() {
462         Locale.setDefault(Locale.US);
463     }
464 
465     /**
466      * Restore the environment.
467      */
468     @AfterAll
469     public static void afterClass() {
470         Locale.setDefault(SAVE_LOCALE);
471     }
472 
473     /**
474      * Verifies the captured alerts.
475      * @param func actual string producer
476      * @param expected the expected string
477      * @throws Exception in case of failure
478      */
479     protected void verify(final Supplier<String> func, final String expected) throws Exception {
480         verify(func, expected, DEFAULT_WAIT_TIME);
481     }
482 
483     /**
484      * Verifies the captured alerts.
485      * @param func actual string producer
486      * @param expected the expected string
487      * @param maxWaitTime the maximum time to wait to get the alerts (in millis)
488      * @throws Exception in case of failure
489      */
490     protected void verify(final Supplier<String> func, final String expected,
491             final Duration maxWaitTime) throws Exception {
492         final long maxWait = System.currentTimeMillis() + maxWaitTime.toMillis();
493 
494         String actual = null;
495         while (System.currentTimeMillis() < maxWait) {
496             actual = func.get();
497 
498             if (Objects.equals(expected, actual)) {
499                 break;
500             }
501 
502             Thread.sleep(50);
503         }
504 
505         assertEquals(expected, actual);
506     }
507 
508     /**
509      * Returns the mock WebConnection instance for the current test.
510      * @return the mock WebConnection instance for the current test
511      */
512     protected MockWebConnection getMockWebConnection() {
513         if (mockWebConnection_ == null) {
514             mockWebConnection_ = new MockWebConnection();
515         }
516         return mockWebConnection_;
517     }
518 
519     /**
520      * Cleanup after a test.
521      */
522     @AfterEach
523     public void releaseResources() {
524         if (mockWebConnection_ != null) {
525             mockWebConnection_.clear();
526         }
527     }
528 
529     /**
530      * Gets the active JavaScript threads.
531      * @return the threads
532      */
533     protected List<Thread> getJavaScriptThreads() {
534         final Thread[] threads = new Thread[Thread.activeCount() + 10];
535         Thread.enumerate(threads);
536         final List<Thread> jsThreads = new ArrayList<>();
537         for (final Thread t : threads) {
538             if (t != null && t.getName().startsWith("JS executor for")) {
539                 jsThreads.add(t);
540             }
541         }
542 
543         return jsThreads;
544     }
545 
546     /**
547      * Read the content of the given file using our classloader.
548      * @param fileName the file name
549      * @return the content as string
550      * @throws IOException in case of error
551      */
552     protected String getFileContent(final String fileName) throws IOException {
553         final InputStream stream = getClass().getClassLoader().getResourceAsStream(fileName);
554         assertNotNull(fileName, stream);
555         return IOUtils.toString(stream, ISO_8859_1);
556     }
557 
558     protected void compareImages(final String expected, final String current) throws IOException {
559         final String currentBase64Image = current.split(",")[1];
560         final byte[] currentImageBytes = Base64.getDecoder().decode(currentBase64Image);
561 
562         try (ByteArrayInputStream currentBis = new ByteArrayInputStream(currentImageBytes)) {
563             final BufferedImage currentImage = ImageIO.read(currentBis);
564 
565             compareImages(expected, current, currentImage);
566         }
567     }
568 
569     protected void compareImages(final String expected,
570             final String current, final BufferedImage currentImage) throws IOException {
571         final String expectedBase64Image = expected.split(",")[1];
572         final byte[] expectedImageBytes = Base64.getDecoder().decode(expectedBase64Image);
573 
574         try (ByteArrayInputStream expectedBis = new ByteArrayInputStream(expectedImageBytes)) {
575             final BufferedImage expectedImage = ImageIO.read(expectedBis);
576 
577             final ImageComparison imageComparison = new ImageComparison(expectedImage, currentImage);
578             // imageComparison.setMinimalRectangleSize(10);
579             imageComparison.setPixelToleranceLevel(0.2);
580             imageComparison.setAllowingPercentOfDifferentPixels(7);
581 
582             final ImageComparisonResult imageComparisonResult = imageComparison.compareImages();
583             final ImageComparisonState imageComparisonState = imageComparisonResult.getImageComparisonState();
584 
585             if (ImageComparisonState.SIZE_MISMATCH == imageComparisonState) {
586                 final String dir = "target/" + testInfo_.getDisplayName();
587                 Files.createDirectories(Paths.get(dir));
588 
589                 final File expectedOut = new File(dir, "expected.png");
590                 final File currentOut = new File(dir, "current.png");
591                 ImageComparisonUtil.saveImage(expectedOut, expectedImage);
592                 ImageComparisonUtil.saveImage(currentOut, currentImage);
593 
594                 String fail = "The images are different in size - "
595                         + "expected: " + expectedImage.getWidth() + "x" + expectedImage.getHeight()
596                         + " current: " + currentImage.getWidth() + "x" + currentImage.getHeight()
597                         + " (expected: " + expectedOut.getAbsolutePath()
598                             + " current: " + currentOut.getAbsolutePath() + ")";
599                 if (current != null) {
600                     fail += "; current data: '" + current + "'";
601                 }
602                 fail(fail);
603             }
604             else if (ImageComparisonState.MISMATCH == imageComparisonState) {
605                 final String dir = "target/" + testInfo_.getDisplayName();
606                 Files.createDirectories(Paths.get(dir));
607 
608                 final File expectedOut = new File(dir, "expected.png");
609                 final File currentOut = new File(dir, "current.png");
610                 final File differenceOut = new File(dir, "difference.png");
611                 ImageComparisonUtil.saveImage(expectedOut, expectedImage);
612                 ImageComparisonUtil.saveImage(currentOut, currentImage);
613                 ImageComparisonUtil.saveImage(differenceOut, imageComparisonResult.getResult());
614 
615                 String fail = "The images are different (expected: " + expectedOut.getAbsolutePath()
616                             + " current: " + currentOut.getAbsolutePath()
617                             + " difference: " + differenceOut.getAbsolutePath() + ")";
618                 if (current != null) {
619                     fail += "; current data: '" + current + "'";
620                 }
621                 fail(fail);
622             }
623         }
624     }
625 }