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