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