1
2
3
4
5
6
7
8
9
10
11
12
13
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
65
66
67
68
69
70
71
72
73
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
83 public static final String DOCTYPE_HTML = "<!DOCTYPE html>\n";
84
85
86
87
88 public TestInfo testInfo_;
89
90
91
92
93
94 private static final Locale SAVE_LOCALE = Locale.getDefault();
95
96
97
98
99 public static final int PORT = Integer.parseInt(System.getProperty("htmlunit.test.port", "22222"));
100
101
102 public static final int PORT2 = Integer.parseInt(System.getProperty("htmlunit.test.port2", "" + (PORT + 1)));
103
104
105 public static final int PORT3 = Integer.parseInt(System.getProperty("htmlunit.test.port3", "" + (PORT + 2)));
106
107
108 public static final int PORT_PRIMITIVE_SERVER = Integer.parseInt(
109 System.getProperty("htmlunit.test.port_primitive", "" + (PORT + 3)));
110
111
112 public static final int PORT_PROXY_SERVER = Integer.parseInt(
113 System.getProperty("htmlunit.test.port_proxy", "" + (PORT + 4)));
114
115
116 public static final int SOCKS_PROXY_PORT = Integer.parseInt(
117 System.getProperty("htmlunit.test.socksproxy.port", "" + (PORT + 5)));
118
119
120 public static final String SOCKS_PROXY_HOST = System.getProperty("htmlunit.test.socksproxy.host", "localhost");
121
122
123 protected static final Duration DEFAULT_WAIT_TIME = Duration.ofSeconds(1);
124
125
126 public static final URL URL_FIRST;
127
128
129 public static final URL URL_SECOND;
130
131
132
133
134
135 public static final URL URL_THIRD;
136
137
138
139
140 public static final URL URL_CROSS_ORIGIN;
141
142
143
144
145
146 public static final URL URL_CROSS_ORIGIN2;
147
148
149
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
168 throw new IllegalStateException("Unable to create URL constants");
169 }
170 }
171
172
173
174
175 protected WebTestCase() {
176 }
177
178 @BeforeEach
179 void init(final TestInfo testInfo) {
180 testInfo_ = testInfo;
181 }
182
183
184
185
186
187 public static void assertNull(final Object object) {
188 Assertions.assertNull(object, "Expected null but found [" + object + "]");
189 }
190
191
192
193
194
195
196 public static void assertNull(final String message, final Object object) {
197 Assertions.assertNull(object, message);
198 }
199
200
201
202
203
204 public static void assertNotNull(final Object object) {
205 Assertions.assertNotNull(object);
206 }
207
208
209
210
211
212
213 public static void assertNotNull(final String message, final Object object) {
214 Assertions.assertNotNull(object, message);
215 }
216
217
218
219
220
221
222 public static void assertSame(final Object expected, final Object actual) {
223 Assertions.assertSame(expected, actual);
224 }
225
226
227
228
229
230
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
238
239
240
241 public static void assertNotSame(final Object expected, final Object actual) {
242 Assertions.assertNotSame(expected, actual);
243 }
244
245
246
247
248
249
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
257
258
259
260
261 protected static void assertEquals(final URL expectedUrl, final URL actualUrl) {
262 Assertions.assertEquals(expectedUrl.toExternalForm(), actualUrl.toExternalForm());
263 }
264
265
266
267
268
269
270 protected static void assertEquals(final Object expected, final Object actual) {
271 Assertions.assertEquals(expected, actual);
272 }
273
274
275
276
277
278
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
286
287
288
289 protected static void assertEquals(final int expected, final int actual) {
290 Assertions.assertEquals(expected, actual);
291 }
292
293
294
295
296
297
298 protected void assertEquals(final boolean expected, final boolean actual) {
299 Assertions.assertEquals(Boolean.valueOf(expected), Boolean.valueOf(actual));
300 }
301
302
303
304
305
306
307
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
315
316
317
318 protected void assertEquals(final String expectedUrl, final URL actualUrl) {
319 Assertions.assertEquals(expectedUrl, actualUrl.toExternalForm());
320 }
321
322
323
324
325
326
327
328
329
330 protected void assertEquals(final String[] expected, final List<String> actual) {
331 assertEquals(null, expected, actual);
332 }
333
334
335
336
337
338
339
340
341
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
349
350
351
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
359
360
361 protected void assertTrue(final boolean condition) {
362 Assertions.assertTrue(condition);
363 }
364
365
366
367
368
369
370 protected void assertTrue(final String message, final boolean condition) {
371 Assertions.assertTrue(condition, message);
372 }
373
374
375
376
377
378 protected void assertFalse(final boolean condition) {
379 Assertions.assertFalse(condition);
380 }
381
382
383
384
385
386
387 protected void assertFalse(final String message, final boolean condition) {
388 Assertions.assertFalse(condition, message);
389 }
390
391
392
393
394
395 public void setBrowserVersion(final BrowserVersion browserVersion) {
396 browserVersion_ = browserVersion;
397 }
398
399
400
401
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
412
413
414 public void setExpectedAlerts(final String... expectedAlerts) {
415 expectedAlerts_ = expectedAlerts;
416 }
417
418
419
420
421
422 protected String[] getExpectedAlerts() {
423 return expectedAlerts_;
424 }
425
426
427
428
429
430 protected void expandExpectedAlertsVariables(final URL url) {
431 expandExpectedAlertsVariables(url.toExternalForm());
432 }
433
434
435
436
437
438 protected void expandExpectedAlertsVariables(final String url) {
439 expandExpectedAlertsVariables(url, PORT);
440 }
441
442
443
444
445
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
459
460
461
462
463 protected <T extends Serializable> T clone(final T object) {
464 return SerializationUtils.clone(object);
465 }
466
467
468
469
470
471 @BeforeAll
472 public static void beforeClass() {
473 Locale.setDefault(Locale.US);
474 }
475
476
477
478
479 @AfterAll
480 public static void afterClass() {
481 Locale.setDefault(SAVE_LOCALE);
482 }
483
484
485
486
487
488
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
496
497
498
499
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
521
522
523 protected MockWebConnection getMockWebConnection() {
524 if (mockWebConnection_ == null) {
525 mockWebConnection_ = new MockWebConnection();
526 }
527 return mockWebConnection_;
528 }
529
530
531
532
533 @AfterEach
534 public void releaseResources() {
535 if (mockWebConnection_ != null) {
536 mockWebConnection_.clear();
537 }
538 }
539
540
541
542
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
559
560
561
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
598 imageComparison.setPixelToleranceLevel(0.15);
599 imageComparison.setAllowingPercentOfDifferentPixels(5);
600
601 final ImageComparisonResult imageComparisonResult = imageComparison.compareImages();
602 final ImageComparisonState imageComparisonState = imageComparisonResult.getImageComparisonState();
603
604
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
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
632 assertEquals(expected, current, fail);
633 }
634 }
635 }
636
637
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
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 }