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  import static java.nio.charset.StandardCharsets.UTF_8;
19  
20  import java.io.File;
21  import java.io.FileInputStream;
22  import java.io.IOException;
23  import java.net.URISyntaxException;
24  import java.net.URL;
25  import java.nio.charset.Charset;
26  import java.nio.file.Files;
27  import java.time.Duration;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collections;
31  import java.util.Enumeration;
32  import java.util.HashMap;
33  import java.util.HashSet;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Map;
37  import java.util.Properties;
38  import java.util.Set;
39  import java.util.concurrent.Executor;
40  import java.util.concurrent.Executors;
41  
42  import org.apache.commons.io.FileUtils;
43  import org.apache.commons.io.IOUtils;
44  import org.apache.commons.lang3.StringUtils;
45  import org.apache.commons.lang3.exception.ExceptionUtils;
46  import org.apache.commons.logging.Log;
47  import org.apache.commons.logging.LogFactory;
48  import org.eclipse.jetty.server.Server;
49  import org.htmlunit.MockWebConnection.RawResponseData;
50  import org.htmlunit.WebServerTestCase.SSLVariant;
51  import org.htmlunit.attachment.DownloadingAttachmentHandler;
52  import org.htmlunit.html.HtmlElement;
53  import org.htmlunit.javascript.JavaScriptEngine;
54  import org.htmlunit.junit.TestCaseCorrector;
55  import org.htmlunit.util.JettyServerUtils;
56  import org.htmlunit.util.NameValuePair;
57  import org.junit.jupiter.api.AfterAll;
58  import org.junit.jupiter.api.AfterEach;
59  import org.junit.jupiter.api.Assertions;
60  import org.junit.jupiter.api.BeforeEach;
61  import org.junit.jupiter.api.extension.ExtendWith;
62  import org.openqa.selenium.Alert;
63  import org.openqa.selenium.By;
64  import org.openqa.selenium.Dimension;
65  import org.openqa.selenium.JavascriptExecutor;
66  import org.openqa.selenium.NoAlertPresentException;
67  import org.openqa.selenium.NoSuchSessionException;
68  import org.openqa.selenium.NoSuchWindowException;
69  import org.openqa.selenium.UnhandledAlertException;
70  import org.openqa.selenium.WebDriver;
71  import org.openqa.selenium.WebDriverException;
72  import org.openqa.selenium.WebElement;
73  import org.openqa.selenium.chrome.ChromeDriver;
74  import org.openqa.selenium.chrome.ChromeDriverService;
75  import org.openqa.selenium.chrome.ChromeOptions;
76  import org.openqa.selenium.devtools.DevTools;
77  import org.openqa.selenium.devtools.v156.emulation.Emulation;
78  import org.openqa.selenium.edge.EdgeDriver;
79  import org.openqa.selenium.edge.EdgeDriverService;
80  import org.openqa.selenium.edge.EdgeOptions;
81  import org.openqa.selenium.firefox.FirefoxDriver;
82  import org.openqa.selenium.firefox.FirefoxDriverService;
83  import org.openqa.selenium.firefox.FirefoxOptions;
84  import org.openqa.selenium.firefox.FirefoxProfile;
85  import org.openqa.selenium.firefox.GeckoDriverService;
86  import org.openqa.selenium.htmlunit.HtmlUnitDriver;
87  import org.openqa.selenium.htmlunit.HtmlUnitWebElement;
88  import org.openqa.selenium.htmlunit.options.HtmlUnitDriverOptions;
89  import org.openqa.selenium.htmlunit.options.HtmlUnitOption;
90  import org.openqa.selenium.remote.UnreachableBrowserException;
91  import org.opentest4j.AssertionFailedError;
92  
93  import jakarta.servlet.Servlet;
94  import jakarta.servlet.ServletException;
95  import jakarta.servlet.http.HttpServlet;
96  import jakarta.servlet.http.HttpServletRequest;
97  import jakarta.servlet.http.HttpServletResponse;
98  
99  /**
100  * Base class for tests using WebDriver.
101  * <p>
102  * By default, this test runs with HtmlUnit, but this behavior can be changed by having a property file named
103  * "{@code test.properties}" in the HtmlUnit root directory.
104  * Sample (remove the part not matching your os):
105  * <pre>
106    browsers=hu,ff,chrome,chrome-bidi
107 
108    ff.bin=/usr/bin/firefox                              [Unix]
109    ff-esr.bin=/usr/bin/firefox-esr                      [Unix]
110    geckodriver.bin=/usr/bin/driver/geckodriver          [Unix]
111    chrome.bin=/path/to/chromedriver                     [Unix]
112    edge.bin=/path/to/chromedriver                       [Unix]
113 
114    geckodriver.bin=C:\\path\\to\\geckodriver.exe              [Windows]
115    ff.bin=C:\\path\\to\\Mozilla Firefox\\firefox.exe          [Windows]
116    ff-esr.bin=C:\\path\\to\\Mozilla Firefox ESR\\firefox.exe  [Windows]
117    chrome.bin=C:\\path\\to\\chromedriver.exe                  [Windows]
118    edge.bin=C:\\path\\to\\msedgedriver.exe                    [Windows]
119 
120    autofix=false
121    </pre>
122 
123  * The file could contain some properties:
124  * <ul>
125  *   <li>browsers: is a comma separated list contains any combination of
126  *     <ul>
127  *       <li>hu (for HtmlUnit with all browser versions),</li>
128  *       <li>hu-ff,</li>
129  *       <li>hu-ff-esr,</li>
130  *       <li>hu-chrome,</li>
131  *       <li>hu-edge,</li>
132  *       <li>ff, (running test using real Firefox),</li>
133  *       <li>ff-esr, (running test using real Firefox ESR),</li>
134  *       <li>chrome (running test using real Chrome),</li>
135  *       <li>edge (running test using real Edge),</li>
136  *       <li>ff-bidi, ff-esr-bidi, chrome-bidi, edge-bidi (the same real browsers but the session is
137  *       started with BiDi enabled; the expectations are the same as for the classic variants),</li>
138  *     </ul>
139  *   </li>
140  *
141  *   <li>chrome.bin (mandatory if it does not exist in the <i>path</i>): is the location of the ChromeDriver binary (see
142  *   <a href="https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json">Chrome Driver downloads</a>)</li>
143  *   <li>geckodriver.bin (mandatory if it does not exist in the <i>path</i>): is the location of the GeckoDriver binary
144  *   (see <a href="https://github.com/mozilla/geckodriver/releases">Gecko Driver Releases</a>)</li>
145  *   <li>ff.bin (optional): is the location of the FF binary, in Windows use double back-slashes</li>
146  *   <li>ff-esr.bin (optional): is the location of the FF binary, in Windows use double back-slashes</li>
147  *   <li>edge.bin (mandatory if it does not exist in the <i>path</i>): is the location of the MicrosoftWebDriver binary
148  *   (see <a href="https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/">Microsoft Edge WebDriver downloads</a>)</li>
149  *   <li>autofix (optional): if {@code true}, try to automatically fix the real browser expectations,
150  *   or add/remove {@code @NotYetImplemented} annotations, use with caution!</li>
151  * </ul>
152  *
153  * @author Marc Guillemot
154  * @author Ahmed Ashour
155  * @author Ronald Brill
156  * @author Frank Danek
157  */
158 @ExtendWith(TestCaseCorrector.class)
159 public abstract class WebDriverTestCase extends WebTestCase {
160 
161     private static final String LOG_EX_FUNCTION =
162             "  function logEx(e) {\n"
163             + "    let toStr = null;\n"
164             + "    if (toStr === null && e instanceof EvalError) { toStr = ''; }\n"
165             + "    if (toStr === null && e instanceof RangeError) { toStr = ''; }\n"
166             + "    if (toStr === null && e instanceof ReferenceError) { toStr = ''; }\n"
167             + "    if (toStr === null && e instanceof SyntaxError) { toStr = ''; }\n"
168             + "    if (toStr === null && e instanceof TypeError) { toStr = ''; }\n"
169             + "    if (toStr === null && e instanceof URIError) { toStr = ''; }\n"
170             + "    if (toStr === null && e instanceof AggregateError) { toStr = '/AggregateError'; }\n"
171             + "    if (toStr === null && typeof InternalError == 'function' "
172                           + "&& e instanceof InternalError) { toStr = '/InternalError'; }\n"
173             + "    if (toStr === null) {\n"
174             + "      let rx = /\\[object (.*)\\]/;\n"
175             + "      toStr = Object.prototype.toString.call(e);\n"
176             + "      let match = rx.exec(toStr);\n"
177             + "      if (match != null) { toStr = '/' + match[1]; }\n"
178             + "    }"
179             + "    log(e.name + toStr);\n"
180             + "  }\n";
181 
182     /**
183      * Function used in many tests.
184      */
185     public static final String LOG_TITLE_FUNCTION =
186             "  function log(msg) { window.document.title += msg + '\\u00a7'; }\n"
187             + LOG_EX_FUNCTION;
188 
189     /**
190      * Function used in many tests.
191      */
192     public static final String LOG_TITLE_FUNCTION_NORMALIZE =
193             "  function log(msg) { "
194                     + "msg = '' + msg; "
195                     + "msg = msg.replace(/ /g, '\\\\s'); "
196                     + "msg = msg.replace(/\\n/g, '\\\\n'); "
197                     + "msg = msg.replace(/\\r/g, '\\\\r'); "
198                     + "msg = msg.replace(/\\t/g, '\\\\t'); "
199                     + "msg = msg.replace(/\\u00a0/g, '\\\\u00a0'); "
200                     + "msg = msg.replace(/\\u001e/g, '\\\\u001e'); "
201                     + "window.document.title += msg + '\u00A7';}\n"
202 
203                     + LOG_EX_FUNCTION;
204 
205     /**
206      * Function used in many tests.
207      */
208     public static final String LOG_WINDOW_NAME_FUNCTION =
209             "  function log(msg) { window.top.name += msg + '\\u00a7'; }\n"
210             + "  window.top.name = '';"
211             + LOG_EX_FUNCTION;
212 
213     /**
214      * Function used in many tests.
215      */
216     public static final String LOG_SESSION_STORAGE_FUNCTION =
217             "  function log(msg) { "
218             + "var l = sessionStorage.getItem('Log');"
219             + "sessionStorage.setItem('Log', (null === l?'':l) + msg + '\\u00a7'); }\n";
220 
221     /**
222      * Function used in many tests.
223      */
224     public static final String LOG_TEXTAREA_FUNCTION = "  function log(msg) { "
225             + "document.getElementById('myLog').value += msg + '\u00A7';}\n"
226             + LOG_EX_FUNCTION;
227 
228     /**
229      * HtmlSniped to insert text area used for logging.
230      */
231     public static final String LOG_TEXTAREA = "  <textarea id='myLog' cols='80' rows='22'></textarea>\n";
232 
233     /**
234      * The system property for automatically fixing the test case expectations.
235      */
236     public static final String AUTOFIX_ = "htmlunit.autofix";
237 
238     /**
239      * All browsers supported.
240      */
241     private static final List<BrowserVersion> ALL_BROWSERS_ = List.of(BrowserVersion.CHROME, BrowserVersion.EDGE, BrowserVersion.FIREFOX, BrowserVersion.FIREFOX_ESR);
242 
243     /**
244      * Browsers which run by default.
245      */
246     private static final BrowserVersion[] DEFAULT_RUNNING_BROWSERS_ =
247         {BrowserVersion.CHROME,
248             BrowserVersion.EDGE,
249             BrowserVersion.FIREFOX,
250             BrowserVersion.FIREFOX_ESR};
251 
252     private static final Log LOG = LogFactory.getLog(WebDriverTestCase.class);
253 
254     private static Set<String> BROWSERS_PROPERTIES_;
255     private static String CHROME_BIN_;
256     private static String EDGE_BIN_;
257     private static String GECKO_BIN_;
258     private static String FF_BIN_;
259     private static String FF_ESR_BIN_;
260 
261     /** The driver cache. */
262     protected static final Map<BrowserVersion, WebDriver> WEB_DRIVERS_ = new HashMap<>();
263 
264     /**
265      * The key of the driver cache for real browsers; the classic and the BiDi variant of a browser
266      * and the drivers configured for downloads need their own driver.
267      */
268     protected record RealBrowserKey(BrowserVersion browserVersion, boolean bidi, boolean downloads) {
269     }
270 
271     /** The download directories of the real browsers (only used by tests that {@link #needDownloads()}). */
272     private static final Map<RealBrowserKey, File> DOWNLOAD_DIRS_ = new HashMap<>();
273 
274     /** The driver cache for real browsers. */
275     protected static final Map<RealBrowserKey, WebDriver> WEB_DRIVERS_REAL_BROWSERS = new HashMap<>();
276     private static final Map<RealBrowserKey, Integer> WEB_DRIVERS_REAL_BROWSERS_USAGE_COUNT = new HashMap<>();
277 
278     private static Server STATIC_SERVER_;
279     private static String STATIC_SERVER_STARTER_; // stack trace to save the server start code location
280     // second server for cross-origin tests.
281     private static Server STATIC_SERVER2_;
282     private static String STATIC_SERVER2_STARTER_; // stack trace to save the server start code location
283     // third server for multi-origin cross-origin tests.
284     private static Server STATIC_SERVER3_;
285     private static String STATIC_SERVER3_STARTER_; // stack trace to save the server start code location
286 
287     private static Boolean LAST_TEST_UsesMockWebConnection_;
288     private static final Executor EXECUTOR_POOL = Executors.newFixedThreadPool(4);
289 
290     private boolean useRealBrowser_;
291     private boolean useBiDi_;
292 
293     /**
294      * The HtmlUnitDriver.
295      */
296     private HtmlUnitDriver webDriver_;
297 
298     /**
299      * Override this function in a test class to ask for STATIC_SERVER2_ to be set up.
300      * @return true if two servers are needed.
301      */
302     protected boolean needThreeConnections() {
303         return false;
304     }
305 
306     /**
307      * Override this function in a test class to run the test with a driver that is able to
308      * handle downloads (attachments).
309      * <ul>
310      *   <li>Real browsers are started with a download directory and without any download prompt;
311      *   such a browser is cached separately, so the other tests are not affected.</li>
312      *   <li>HtmlUnit gets a {@link DownloadingAttachmentHandler} that stores the files in a download
313      *   directory, so HtmlUnit and the real browsers are checked the same way.</li>
314      * </ul>
315      * Use {@link #waitForDownloads(int, Duration)} to get the names of the downloaded files.
316      * @return true if the test class needs download support
317      */
318     protected boolean needDownloads() {
319         return false;
320     }
321 
322     /**
323      * @return the download directory of the browser (HtmlUnit or real browser) or {@code null}
324      *         if {@link #needDownloads()} is {@code false}
325      */
326     protected final File getDownloadDirectory() {
327         if (!needDownloads()) {
328             return null;
329         }
330         synchronized (WEB_DRIVERS_REAL_BROWSERS) {
331             return DOWNLOAD_DIRS_.get(realBrowserKey());
332         }
333     }
334 
335     /**
336      * Waits for the downloads of the test and returns their file names (sorted).
337      * Unfinished downloads of the real browsers (like {@code *.crdownload}) are ignored.
338      * Returns what is available after the max wait time if there are fewer than expected downloads;
339      * assert the result.
340      *
341      * @param expectedCount the number of expected downloads
342      * @param maxWaitTime the maximum time to wait
343      * @return the sorted file names
344      * @throws InterruptedException in case of an interrupt
345      */
346     protected final List<String> waitForDownloads(final int expectedCount, final Duration maxWaitTime)
347             throws InterruptedException {
348         final long maxWait = System.currentTimeMillis() + maxWaitTime.toMillis();
349 
350         List<String> names = currentDownloads();
351         while (names.size() < expectedCount && System.currentTimeMillis() < maxWait) {
352             Thread.sleep(10);
353             names = currentDownloads();
354         }
355         return names;
356     }
357 
358     private List<String> currentDownloads() {
359         final List<String> names = new ArrayList<>();
360         final File dir = getDownloadDirectory();
361         final File[] files = dir == null ? null : dir.listFiles();
362         if (files != null) {
363             for (final File file : files) {
364                 final String name = file.getName();
365                 // unfinished downloads
366                 if (file.isFile()
367                         && !name.endsWith(".crdownload") && !name.endsWith(".part")
368                         && !name.endsWith(".tmp") && !name.endsWith(".download")) {
369                     names.add(name);
370                 }
371             }
372         }
373         Collections.sort(names);
374         return names;
375     }
376 
377     private void clearDownloads() {
378         final File dir = getDownloadDirectory();
379         if (dir != null) {
380             final File[] files = dir.listFiles();
381             if (files != null) {
382                 for (final File file : files) {
383                     FileUtils.deleteQuietly(file);
384                 }
385             }
386         }
387     }
388 
389     private File createDownloadDirectory() throws IOException {
390         synchronized (WEB_DRIVERS_REAL_BROWSERS) {
391             File dir = DOWNLOAD_DIRS_.get(realBrowserKey());
392             if (dir == null) {
393                 dir = Files.createTempDirectory("htmlunit-downloads-").toFile();
394                 DOWNLOAD_DIRS_.put(realBrowserKey(), dir);
395             }
396             return dir;
397         }
398     }
399 
400     private void addChromeDownloadPreferences(final Map<String, Object> prefs) throws IOException {
401         if (needDownloads()) {
402             prefs.put("download.default_directory", createDownloadDirectory().getAbsolutePath());
403             prefs.put("download.prompt_for_download", false);
404             prefs.put("download.directory_upgrade", true);
405         }
406     }
407 
408     /**
409      * @return the browser properties (and initializes them lazy)
410      */
411     public static Set<String> getBrowsersProperties() {
412         if (BROWSERS_PROPERTIES_ == null) {
413             try {
414                 final Properties properties = new Properties();
415                 final File file = new File("test.properties");
416                 if (file.exists()) {
417                     try (FileInputStream in = new FileInputStream(file)) {
418                         properties.load(in);
419                     }
420 
421                     String browsersValue = properties.getProperty("browsers");
422                     if (browsersValue == null || browsersValue.isEmpty()) {
423                         browsersValue = "hu";
424                     }
425                     BROWSERS_PROPERTIES_ = new HashSet<>(Arrays.asList(browsersValue.replaceAll(" ", "")
426                             .toLowerCase(Locale.ROOT).split(",")));
427                     CHROME_BIN_ = properties.getProperty("chrome.bin");
428                     EDGE_BIN_ = properties.getProperty("edge.bin");
429 
430                     GECKO_BIN_ = properties.getProperty("geckodriver.bin");
431                     FF_BIN_ = properties.getProperty("ff.bin");
432                     FF_ESR_BIN_ = properties.getProperty("ff-esr.bin");
433 
434                     final boolean autofix = Boolean.parseBoolean(properties.getProperty("autofix"));
435                     System.setProperty(AUTOFIX_, Boolean.toString(autofix));
436                 }
437             }
438             catch (final Exception e) {
439                 LOG.error("Error reading htmlunit.properties. Ignoring!", e);
440             }
441             if (BROWSERS_PROPERTIES_ == null) {
442                 BROWSERS_PROPERTIES_ = new HashSet<>(Arrays.asList("hu"));
443             }
444             if (BROWSERS_PROPERTIES_.contains("hu")) {
445                 for (final BrowserVersion browserVersion : DEFAULT_RUNNING_BROWSERS_) {
446                     BROWSERS_PROPERTIES_.add("hu-" + browserVersion.getNickname().toLowerCase());
447                 }
448             }
449         }
450         return BROWSERS_PROPERTIES_;
451     }
452 
453     /**
454      * @return the list of supported browsers
455      */
456     public static List<BrowserVersion> allBrowsers() {
457         return ALL_BROWSERS_;
458     }
459 
460     /**
461      * Configure the driver only once.
462      * @return the driver
463      */
464     protected WebDriver getWebDriver() {
465         final BrowserVersion browserVersion = getBrowserVersion();
466         final RealBrowserKey realBrowserKey = realBrowserKey();
467         WebDriver driver;
468         if (useRealBrowser()) {
469             synchronized (WEB_DRIVERS_REAL_BROWSERS) {
470                 driver = WEB_DRIVERS_REAL_BROWSERS.get(realBrowserKey);
471                 if (driver != null) {
472                     // there seems to be a memory leak at least with FF;
473                     // we have to restart sometimes
474                     Integer count = WEB_DRIVERS_REAL_BROWSERS_USAGE_COUNT.get(realBrowserKey);
475                     if (null == count) {
476                         count = -1;
477                     }
478                     count += 1;
479                     if (count >= 1000) {
480                         shutDownReal(realBrowserKey);
481                         driver = null;
482                     }
483                     else {
484                         WEB_DRIVERS_REAL_BROWSERS_USAGE_COUNT.put(realBrowserKey, count);
485                     }
486                 }
487 
488                 if (driver == null) {
489                     try {
490                         driver = buildWebDriver();
491                     }
492                     catch (final IOException e) {
493                         throw new RuntimeException(e);
494                     }
495 
496                     WEB_DRIVERS_REAL_BROWSERS.put(realBrowserKey, driver);
497                     WEB_DRIVERS_REAL_BROWSERS_USAGE_COUNT.put(realBrowserKey, 0);
498                 }
499             }
500         }
501         else {
502             driver = WEB_DRIVERS_.get(browserVersion);
503             if (driver == null) {
504                 try {
505                     driver = buildWebDriver();
506                 }
507                 catch (final IOException e) {
508                     throw new RuntimeException(e);
509                 }
510 
511                 if (isWebClientCached()) {
512                     WEB_DRIVERS_.put(browserVersion, driver);
513                 }
514             }
515         }
516         return driver;
517     }
518 
519     /**
520      * Closes the drivers.
521      * @throws Exception If an error occurs
522      */
523     @AfterAll
524     public static void shutDownAll() throws Exception {
525         for (final WebDriver driver : WEB_DRIVERS_.values()) {
526             driver.quit();
527         }
528         WEB_DRIVERS_.clear();
529 
530         shutDownRealBrowsers();
531 
532         synchronized (WEB_DRIVERS_REAL_BROWSERS) {
533             for (final File dir : DOWNLOAD_DIRS_.values()) {
534                 FileUtils.deleteQuietly(dir);
535             }
536             DOWNLOAD_DIRS_.clear();
537         }
538 
539         stopWebServers();
540     }
541 
542     /**
543      * Closes the real browser drivers.
544      */
545     private static void shutDownRealBrowsers() {
546         synchronized (WEB_DRIVERS_REAL_BROWSERS) {
547             for (final WebDriver driver : WEB_DRIVERS_REAL_BROWSERS.values()) {
548                 quit(driver);
549             }
550             WEB_DRIVERS_REAL_BROWSERS.clear();
551             WEB_DRIVERS_REAL_BROWSERS_USAGE_COUNT.clear();
552         }
553     }
554 
555     /**
556      * Closes the real browser driver.
557      * @param key the real browser to close
558      */
559     private static void shutDownReal(final RealBrowserKey key) {
560         synchronized (WEB_DRIVERS_REAL_BROWSERS) {
561             final WebDriver driver = WEB_DRIVERS_REAL_BROWSERS.get(key);
562             if (driver != null) {
563                 quit(driver);
564                 WEB_DRIVERS_REAL_BROWSERS.remove(key);
565                 WEB_DRIVERS_REAL_BROWSERS_USAGE_COUNT.remove(key);
566             }
567         }
568     }
569 
570     private static void quit(final WebDriver driver) {
571         if (driver != null) {
572             try {
573                 driver.quit();
574             }
575             catch (final UnreachableBrowserException e) {
576                 LOG.error("Can't quit browser", e);
577                 // ignore, the browser is gone
578             }
579             catch (final NoClassDefFoundError e) {
580                 LOG.error("Can't quit browser", e);
581                 // ignore, the browser is gone
582             }
583             catch (final UnsatisfiedLinkError e) {
584                 LOG.error("Can't quit browser", e);
585                 // ignore, the browser is gone
586             }
587         }
588     }
589 
590     /**
591      * Asserts all static servers are null.
592      * @throws Exception if it fails
593      */
594     protected static void assertWebServersStopped() throws Exception {
595         Assertions.assertNull(STATIC_SERVER_, STATIC_SERVER_STARTER_);
596         Assertions.assertNull(STATIC_SERVER2_, STATIC_SERVER2_STARTER_);
597         Assertions.assertNull(STATIC_SERVER3_, STATIC_SERVER3_STARTER_);
598     }
599 
600     /**
601      * Stops all WebServers.
602      * @throws Exception if it fails
603      */
604     protected static void stopWebServers() throws Exception {
605         JettyServerUtils.stopServer(STATIC_SERVER_);
606         STATIC_SERVER_ = null;
607 
608         JettyServerUtils.stopServer(STATIC_SERVER2_);
609         STATIC_SERVER2_ = null;
610 
611         JettyServerUtils.stopServer(STATIC_SERVER3_);
612         STATIC_SERVER3_ = null;
613 
614         LAST_TEST_UsesMockWebConnection_ = null;
615     }
616 
617     /**
618      * @return whether to use real browser or not.
619      */
620     public boolean useRealBrowser() {
621         return useRealBrowser_;
622     }
623 
624     /**
625      * Sets whether to use real browser or not.
626      * @param useRealBrowser whether to use real browser or not
627      */
628     public void setUseRealBrowser(final boolean useRealBrowser) {
629         useRealBrowser_ = useRealBrowser;
630     }
631 
632     /**
633      * @return whether the real browser is started with BiDi enabled or not.
634      */
635     public boolean useBiDi() {
636         return useBiDi_;
637     }
638 
639     /**
640      * Sets whether the real browser is started with BiDi enabled or not.
641      * @param useBiDi whether to enable BiDi or not
642      */
643     public void setUseBiDi(final boolean useBiDi) {
644         useBiDi_ = useBiDi;
645     }
646 
647     private RealBrowserKey realBrowserKey() {
648         return new RealBrowserKey(getBrowserVersion(), useBiDi(), needDownloads());
649     }
650 
651     /**
652      * When using a real browser seep for some timem.
653      *
654      * @param  millis the length of time to sleep in milliseconds
655      * @throws InterruptedException in case of an interrupt
656      */
657     public void sleepRealBrowser(final long millis) throws InterruptedException {
658         if (useRealBrowser()) {
659             Thread.sleep(millis);
660         }
661     }
662 
663     /**
664      * Builds a new WebDriver instance.
665      * @return the instance
666      * @throws IOException in case of exception
667      */
668     protected WebDriver buildWebDriver() throws IOException {
669         if (useRealBrowser()) {
670             if (BrowserVersion.EDGE.isSameBrowser(getBrowserVersion())) {
671                 final EdgeDriverService service = new EdgeDriverService.Builder()
672                         .withLogOutput(System.out)
673                         .usingDriverExecutable(new File(EDGE_BIN_))
674 
675                         .withAppendLog(true)
676                         .withReadableTimestamp(true)
677 
678                         .build();
679 
680                 final String locale = getBrowserVersion().getBrowserLocale().toLanguageTag();
681 
682                 final EdgeOptions options = new EdgeOptions();
683                 if (useBiDi()) {
684                     options.setCapability("webSocketUrl", true);
685                 }
686 
687                 options.addArguments("--lang=" + locale);
688                 // https://stackoverflow.com/questions/11289597/webdriver-how-to-specify-preferred-languages-for-chrome
689                 final Map<String, Object> prefs = new HashMap<>();
690                 prefs.put("intl.accept_languages", locale);
691                 addChromeDownloadPreferences(prefs);
692                 options.setExperimentalOption("prefs", prefs);
693                 options.addArguments("--remote-allow-origins=*");
694 
695                 // seems to be not required for edge
696                 // options.addArguments("--disable-search-engine-choice-screen");
697                 // see https://www.selenium.dev/blog/2024/chrome-browser-woes/
698                 // options.addArguments("--disable-features=OptimizationGuideModelDownloading,"
699                 //         + "OptimizationHintsFetching,OptimizationTargetPrediction,OptimizationHints");
700 
701                 final EdgeDriver edge = new EdgeDriver(service, options);
702 
703                 final DevTools devTools = edge.getDevTools();
704                 devTools.createSession();
705 
706                 final String tz = getBrowserVersion().getSystemTimezone().getID();
707                 devTools.send(Emulation.setTimezoneOverride(tz));
708 
709                 return edge;
710             }
711 
712             if (BrowserVersion.CHROME.isSameBrowser(getBrowserVersion())) {
713                 final ChromeDriverService service = new ChromeDriverService.Builder()
714                         .withLogOutput(System.out)
715                         .usingDriverExecutable(new File(CHROME_BIN_))
716 
717                         .withAppendLog(true)
718                         .withReadableTimestamp(true)
719 
720                         .build();
721 
722                 final String locale = getBrowserVersion().getBrowserLocale().toLanguageTag();
723 
724                 final ChromeOptions options = new ChromeOptions();
725                 if (useBiDi()) {
726                     options.setCapability("webSocketUrl", true);
727                 }
728 
729                 options.addArguments("--lang=" + locale);
730                 // https://stackoverflow.com/questions/11289597/webdriver-how-to-specify-preferred-languages-for-chrome
731                 final Map<String, Object> prefs = new HashMap<>();
732                 prefs.put("intl.accept_languages", locale);
733                 addChromeDownloadPreferences(prefs);
734                 options.setExperimentalOption("prefs", prefs);
735                 options.addArguments("--remote-allow-origins=*");
736                 options.addArguments("--disable-search-engine-choice-screen");
737                 // see https://www.selenium.dev/blog/2024/chrome-browser-woes/
738                 options.addArguments("--disable-features=OptimizationGuideModelDownloading,"
739                         + "OptimizationHintsFetching,OptimizationTargetPrediction,OptimizationHints");
740 
741                 final ChromeDriver chrome = new ChromeDriver(service, options);
742 
743                 final DevTools devTools = chrome.getDevTools();
744                 devTools.createSession();
745 
746                 final String tz = getBrowserVersion().getSystemTimezone().getID();
747                 devTools.send(Emulation.setTimezoneOverride(tz));
748 
749                 return chrome;
750             }
751 
752             if (BrowserVersion.FIREFOX.isSameBrowser(getBrowserVersion())) {
753                 return createFirefoxDriver(GECKO_BIN_, FF_BIN_, useBiDi());
754             }
755 
756             if (BrowserVersion.FIREFOX_ESR.isSameBrowser(getBrowserVersion())) {
757                 return createFirefoxDriver(GECKO_BIN_, FF_ESR_BIN_, useBiDi());
758             }
759 
760             throw new RuntimeException("Unexpected BrowserVersion: " + getBrowserVersion());
761         }
762 
763         if (webDriver_ == null) {
764             final HtmlUnitDriverOptions driverOptions = new HtmlUnitDriverOptions(getBrowserVersion());
765 
766             if (isWebClientCached()) {
767                 driverOptions.setCapability(HtmlUnitOption.optHistorySizeLimit, 0);
768             }
769 
770             if (getWebClientTimeout() != null) {
771                 driverOptions.setCapability(HtmlUnitOption.optTimeout, getWebClientTimeout());
772             }
773             webDriver_ = new HtmlUnitDriver(driverOptions);
774             webDriver_.setExecutor(EXECUTOR_POOL);
775 
776             if (needDownloads()) {
777                 try {
778                     webDriver_.getWebClient().setAttachmentHandler(
779                             new DownloadingAttachmentHandler(createDownloadDirectory().toPath()));
780                 }
781                 catch (final IOException e) {
782                     throw new RuntimeException(e);
783                 }
784             }
785         }
786         return webDriver_;
787     }
788 
789     private FirefoxDriver createFirefoxDriver(final String geckodriverBinary, final String binary,
790             final boolean bidi) throws IOException {
791         final FirefoxDriverService service = new GeckoDriverService.Builder()
792                 .withLogOutput(System.out)
793                 .usingDriverExecutable(new File(geckodriverBinary))
794                 .build();
795 
796         final FirefoxOptions options = new FirefoxOptions();
797         options.addPreference("browser.shell.checkDefaultBrowser", false);
798         options.addPreference("browser.startup.homepage_welcomeurl", "about:blank");
799         options.addPreference("startup.homepage_welcome_url", "about:blank");
800         options.addPreference("startup.homepage_override_url", "about:blank");
801         options.addPreference("browser.aboutwelcome.enabled", false);
802 
803         if (bidi) {
804             options.setCapability("webSocketUrl", true);
805         }
806 
807         options.setBinary(binary);
808 
809         String locale = getBrowserVersion().getBrowserLocale().toLanguageTag();
810         locale = locale + "," + getBrowserVersion().getBrowserLocale().getLanguage();
811 
812         final FirefoxProfile profile = new FirefoxProfile();
813         profile.setPreference("intl.accept_languages", locale);
814         if (needDownloads()) {
815             profile.setPreference("browser.download.folderList", 2);
816             profile.setPreference("browser.download.dir", createDownloadDirectory().getAbsolutePath());
817             profile.setPreference("browser.download.useDownloadDir", true);
818             profile.setPreference("browser.download.manager.showWhenStarting", false);
819             profile.setPreference("browser.helperApps.neverAsk.saveToDisk",
820                     "application/octet-stream,application/pdf,application/zip,text/plain,text/html,text/csv");
821             profile.setPreference("pdfjs.disabled", true);
822         }
823         // no idea so far how to set this
824         // final String tz = getBrowserVersion().getSystemTimezone().getID();
825         // profile.setPreference("intl.tz", tz);
826         options.setProfile(profile);
827 
828         return new FirefoxDriver(service, options);
829     }
830 
831     /**
832      * Starts the web server delivering response from the provided connection.
833      * @param mockConnection the sources for responses
834      * @param serverCharset the {@link Charset} at the server side
835      * @throws Exception if a problem occurs
836      */
837     protected void startWebServer(final MockWebConnection mockConnection, final Charset serverCharset)
838             throws Exception {
839         if (Boolean.FALSE.equals(LAST_TEST_UsesMockWebConnection_)) {
840             stopWebServers();
841         }
842 
843         // The mock connection servlet call site under both servers, so long as tests
844         // keep the URLs distinct.
845         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
846         servlets.put("/*", MockWebConnectionServlet.class);
847 
848         LAST_TEST_UsesMockWebConnection_ = Boolean.TRUE;
849 
850         if (STATIC_SERVER_ == null) {
851 
852             final Server server = JettyServerUtils.startWebServer(PORT, "./", servlets, serverCharset, isBasicAuthentication(), SSLVariant.NONE);
853             STATIC_SERVER_STARTER_ = ExceptionUtils.getStackTrace(new Throwable("StaticServerStarter"));
854             STATIC_SERVER_ = server;
855         }
856         MockWebConnectionServlet.MockConnection_ = mockConnection;
857 
858         if (STATIC_SERVER2_ == null && needThreeConnections()) {
859             final Server server2 = JettyServerUtils.startWebServer(PORT2, "./", servlets, null, false, SSLVariant.NONE);
860             STATIC_SERVER2_STARTER_ = ExceptionUtils.getStackTrace(new Throwable("StaticServer2Starter"));
861             STATIC_SERVER2_ = server2;
862 
863             final Server server3 = JettyServerUtils.startWebServer(PORT3, "./", servlets, null, false, SSLVariant.NONE);
864             STATIC_SERVER3_STARTER_ = ExceptionUtils.getStackTrace(new Throwable("StaticServer3Starter"));
865             STATIC_SERVER3_ = server3;
866         }
867     }
868 
869     /**
870      * Returns whether to use basic authentication for all resources or not.
871      * The default implementation returns false.
872      * @return whether to use basic authentication or not
873      */
874     protected boolean isBasicAuthentication() {
875         return false;
876     }
877 
878     /**
879      * Starts the web server on the default {@link #PORT}.
880      * The given resourceBase is used to be the ROOT directory that serves the default context.
881      * <p><b>Don't forget to stop the returned HttpServer after the test</b>
882      *
883      * @param resourceBase the base of resources for the default context
884      * @param servlets map of {String, Class} pairs: String is the path spec, while class is the class
885      * @throws Exception if the test fails
886      */
887     protected static void startWebServer(final String resourceBase, final Map<String, Class<? extends Servlet>> servlets) throws Exception {
888         stopWebServers();
889         LAST_TEST_UsesMockWebConnection_ = Boolean.FALSE;
890 
891         STATIC_SERVER_STARTER_ = ExceptionUtils.getStackTrace(new Throwable("StaticServerStarter"));
892         STATIC_SERVER_ = JettyServerUtils.startWebServer(PORT, resourceBase, servlets, null, false, SSLVariant.NONE);
893     }
894 
895     /**
896      * Starts the <b>second</b> web server on the default {@link #PORT2}.
897      * The given resourceBase is used to be the ROOT directory that serves the default context.
898      * <p><b>Don't forget to stop the returned HttpServer after the test</b>
899      *
900      * @param resourceBase the base of resources for the default context
901      * @param servlets map of {String, Class} pairs: String is the path spec, while class is the class
902      * @throws Exception if the test fails
903      */
904     protected static void startWebServer2(final String resourceBase, final Map<String, Class<? extends Servlet>> servlets) throws Exception {
905         if (STATIC_SERVER2_ != null) {
906             JettyServerUtils.stopServer(STATIC_SERVER2_);
907         }
908         STATIC_SERVER2_STARTER_ = ExceptionUtils.getStackTrace(new Throwable("StaticServer2Starter"));
909         STATIC_SERVER2_ = JettyServerUtils.startWebServer(PORT2, resourceBase, servlets, null, false, SSLVariant.NONE);
910     }
911 
912     /**
913      * Servlet delivering content from a MockWebConnection.
914      */
915     public static class MockWebConnectionServlet extends HttpServlet {
916         private static MockWebConnection MockConnection_;
917 
918         public static void setMockconnection(final MockWebConnection connection) {
919             MockConnection_ = connection;
920         }
921 
922         /**
923          * {@inheritDoc}
924          */
925         @Override
926         protected void service(final HttpServletRequest request, final HttpServletResponse response)
927                 throws ServletException, IOException {
928 
929             try {
930                 doService(request, response);
931             }
932             catch (final ServletException e) {
933                 throw e;
934             }
935             catch (final IOException e) {
936                 throw e;
937             }
938             catch (final Exception e) {
939                 throw new ServletException(e);
940             }
941         }
942 
943         private static void doService(final HttpServletRequest request, final HttpServletResponse response)
944                 throws Exception {
945             String url = request.getRequestURL().toString();
946             if (LOG.isDebugEnabled()) {
947                 LOG.debug(request.getMethod() + " " + url);
948             }
949 
950             if (url.endsWith("/favicon.ico")) {
951                 response.setStatus(HttpServletResponse.SC_NOT_FOUND);
952                 return;
953             }
954 
955             if (url.contains("/delay")) {
956                 final String delay = StringUtils.substringBetween(url, "/delay", "/");
957                 final int ms = Integer.parseInt(delay);
958                 if (LOG.isDebugEnabled()) {
959                     LOG.debug("Sleeping for " + ms + " before to deliver " + url);
960                 }
961                 Thread.sleep(ms);
962             }
963 
964             // copy parameters
965             final List<NameValuePair> requestParameters = new ArrayList<>();
966             try {
967                 for (final Enumeration<String> paramNames = request.getParameterNames();
968                         paramNames.hasMoreElements();) {
969                     final String name = paramNames.nextElement();
970                     final String[] values = request.getParameterValues(name);
971                     for (final String value : values) {
972                         requestParameters.add(new NameValuePair(name, value));
973                     }
974                 }
975             }
976             catch (final IllegalArgumentException e) {
977                 // Jetty 8.1.7 throws it in getParameterNames for a query like "cb=%%RANDOM_NUMBER%%"
978                 // => we should use a more low level test server
979                 requestParameters.clear();
980                 final String query = request.getQueryString();
981                 if (query != null) {
982                     url += "?" + query;
983                 }
984             }
985 
986             final String queryString = request.getQueryString();
987             if (StringUtils.isNotBlank(queryString)) {
988                 url = url + "?" + queryString;
989             }
990             final URL requestedUrl = new URL(url);
991             final WebRequest webRequest = new WebRequest(requestedUrl);
992 
993             final String method = request.getMethod().toUpperCase(Locale.ROOT);
994             webRequest.setHttpMethod(HttpMethod.valueOf(method));
995 
996             // copy headers
997             for (final Enumeration<String> en = request.getHeaderNames(); en.hasMoreElements();) {
998                 final String headerName = en.nextElement();
999                 final String headerValue = request.getHeader(headerName);
1000                 webRequest.setAdditionalHeader(headerName, headerValue);
1001             }
1002 
1003             if (requestParameters.isEmpty() && request.getContentLength() > 0) {
1004                 final byte[] buffer = new byte[request.getContentLength()];
1005                 IOUtils.read(request.getInputStream(), buffer, 0, buffer.length);
1006 
1007                 final String encoding = request.getCharacterEncoding();
1008                 if (encoding == null) {
1009                     webRequest.setRequestBody(new String(buffer, ISO_8859_1));
1010                     webRequest.setCharset(null);
1011                 }
1012                 else {
1013                     webRequest.setRequestBody(new String(buffer, encoding));
1014                     webRequest.setCharset(Charset.forName(encoding));
1015                 }
1016             }
1017             else {
1018                 webRequest.setRequestParameters(requestParameters);
1019             }
1020 
1021             // check content type if it is multipart enctype
1022             if (request.getContentType() != null
1023                         && request.getContentType().startsWith(FormEncodingType.MULTIPART.getName())) {
1024                 webRequest.setEncodingType(FormEncodingType.MULTIPART);
1025             }
1026 
1027             final RawResponseData resp = MockConnection_.getRawResponse(webRequest);
1028 
1029             // write WebResponse to HttpServletResponse
1030             response.setStatus(resp.getStatusCode());
1031 
1032             boolean charsetInContentType = false;
1033             for (final NameValuePair responseHeader : resp.getHeaders()) {
1034                 final String headerName = responseHeader.getName();
1035                 if (HttpHeader.CONTENT_TYPE.equals(headerName) && responseHeader.getValue().contains("charset=")) {
1036                     charsetInContentType = true;
1037                 }
1038                 response.addHeader(headerName, responseHeader.getValue());
1039             }
1040 
1041             if (resp.getByteContent() != null) {
1042                 response.getOutputStream().write(resp.getByteContent());
1043             }
1044             else {
1045                 if (!charsetInContentType) {
1046                     response.setCharacterEncoding(resp.getCharset().name());
1047                 }
1048                 response.getWriter().print(resp.getStringContent());
1049             }
1050             response.flushBuffer();
1051         }
1052     }
1053 
1054     /**
1055      * Same as {@link #loadPageWithAlerts2(String)}... but doesn't verify the alerts.
1056      * @param html the HTML to use
1057      * @return the web driver
1058      * @throws Exception if something goes wrong
1059      */
1060     protected final WebDriver loadPage2(final String html) throws Exception {
1061         return loadPage2(html, URL_FIRST);
1062     }
1063 
1064     /**
1065      * Same as {@link #loadPageWithAlerts2(String)}... but doesn't verify the alerts.
1066      * @param html the HTML to use
1067      * @param url the url to use to load the page
1068      * @return the web driver
1069      * @throws Exception if something goes wrong
1070      */
1071     protected final WebDriver loadPage2(final String html, final URL url) throws Exception {
1072         return loadPage2(html, url, "text/html;charset=ISO-8859-1", ISO_8859_1, null);
1073     }
1074 
1075     /**
1076      * Same as {@link #loadPageWithAlerts2(String)}... but doesn't verify the alerts.
1077      * @param html the HTML to use
1078      * @param url the url to use to load the page
1079      * @param contentType the content type to return
1080      * @param charset the charset
1081      * @return the web driver
1082      * @throws Exception if something goes wrong
1083      */
1084     protected final WebDriver loadPage2(final String html, final URL url,
1085             final String contentType, final Charset charset) throws Exception {
1086         return loadPage2(html, url, contentType, charset, null);
1087     }
1088 
1089     /**
1090      * Same as {@link #loadPageWithAlerts2(String)}... but doesn't verify the alerts.
1091      * @param html the HTML to use
1092      * @param url the url to use to load the page
1093      * @param contentType the content type to return
1094      * @param charset the charset
1095      * @param serverCharset the charset at the server side.
1096      * @return the web driver
1097      * @throws Exception if something goes wrong
1098      */
1099     protected final WebDriver loadPage2(final String html, final URL url,
1100             final String contentType, final Charset charset, final Charset serverCharset) throws Exception {
1101         getMockWebConnection().setResponse(url, html, contentType, charset);
1102         return loadPage2(url, serverCharset);
1103     }
1104 
1105     /**
1106      * Load the page from the url.
1107      * @param url the url to use to load the page
1108      * @param serverCharset the charset at the server side.
1109      * @return the web driver
1110      * @throws Exception if something goes wrong
1111      */
1112     protected final WebDriver loadPage2(final URL url, final Charset serverCharset) throws Exception {
1113         startWebServer(getMockWebConnection(), serverCharset);
1114 
1115         WebDriver driver = getWebDriver();
1116         if (!(driver instanceof HtmlUnitDriver)) {
1117             try {
1118                 resizeIfNeeded(driver);
1119             }
1120             catch (final NoSuchSessionException e) {
1121                 // maybe the driver was killed by the test before; setup a new one
1122                 shutDownRealBrowsers();
1123 
1124                 driver = getWebDriver();
1125                 resizeIfNeeded(driver);
1126             }
1127         }
1128         driver.get(url.toExternalForm());
1129 
1130         return driver;
1131     }
1132 
1133     /**
1134      * Same as {@link #loadPage2(String)} with additional servlet configuration.
1135      * @param html the HTML to use for the default response
1136      * @param servlets the additional servlets to configure with their mapping
1137      * @return the web driver
1138      * @throws Exception if something goes wrong
1139      */
1140     protected final WebDriver loadPage2(final String html,
1141             final Map<String, Class<? extends Servlet>> servlets) throws Exception {
1142         return loadPage2(html, URL_FIRST, servlets, null);
1143     }
1144 
1145     /**
1146      * Same as {@link #loadPage2(String, URL)}, but with additional servlet configuration.
1147      * @param html the HTML to use for the default page
1148      * @param url the URL to use to load the page
1149      * @param servlets the additional servlets to configure with their mapping
1150      * @return the web driver
1151      * @throws Exception if something goes wrong
1152      */
1153     protected final WebDriver loadPage2(final String html, final URL url,
1154             final Map<String, Class<? extends Servlet>> servlets) throws Exception {
1155         return loadPage2(html, url, servlets, null);
1156     }
1157 
1158     /**
1159      * Same as {@link #loadPage2(String, URL)}, but with additional servlet configuration.
1160      * @param html the HTML to use for the default page
1161      * @param url the URL to use to load the page
1162      * @param servlets the additional servlets to configure with their mapping
1163      * @param servlets2 the additional servlets to configure with their mapping for a second server
1164      * @return the web driver
1165      * @throws Exception if something goes wrong
1166      */
1167     protected final WebDriver loadPage2(final String html, final URL url,
1168             final Map<String, Class<? extends Servlet>> servlets,
1169             final Map<String, Class<? extends Servlet>> servlets2) throws Exception {
1170         servlets.put("/*", MockWebConnectionServlet.class);
1171         getMockWebConnection().setResponse(url, html);
1172         MockWebConnectionServlet.MockConnection_ = getMockWebConnection();
1173 
1174         startWebServer("./", servlets);
1175         if (servlets2 != null) {
1176             startWebServer2("./", servlets2);
1177         }
1178 
1179         WebDriver driver = getWebDriver();
1180         if (!(driver instanceof HtmlUnitDriver)) {
1181             try {
1182                 resizeIfNeeded(driver);
1183             }
1184             catch (final NoSuchSessionException e) {
1185                 // maybe the driver was killed by the test before; setup a new one
1186                 shutDownRealBrowsers();
1187 
1188                 driver = getWebDriver();
1189                 resizeIfNeeded(driver);
1190             }
1191         }
1192         driver.get(url.toExternalForm());
1193 
1194         return driver;
1195     }
1196 
1197     protected void resizeIfNeeded(final WebDriver driver) {
1198         final Dimension size = driver.manage().window().getSize();
1199         if (size.getWidth() != 1272 || size.getHeight() != 768) {
1200             // only resize if needed because it may be quite expensive
1201             driver.manage().window().setSize(new Dimension(1272, 768));
1202         }
1203     }
1204 
1205     /**
1206      * Defines the provided HTML as the response for {@link WebTestCase#URL_FIRST}
1207      * and loads the page with this URL using the current WebDriver version; finally, asserts that the
1208      * alerts equal the expected alerts.
1209      * @param html the HTML to use
1210      * @return the web driver
1211      * @throws Exception if something goes wrong
1212      */
1213     protected final WebDriver loadPageWithAlerts2(final String html) throws Exception {
1214         return loadPageWithAlerts2(html, URL_FIRST, DEFAULT_WAIT_TIME);
1215     }
1216 
1217     /**
1218      * Defines the provided HTML as the response for {@link WebTestCase#URL_FIRST}
1219      * and loads the page with this URL using the current WebDriver version; finally, asserts that the
1220      * alerts equal the expected alerts.
1221      * @param html the HTML to use
1222      * @return the web driver
1223      * @throws Exception if something goes wrong
1224      */
1225     protected final WebDriver loadPageVerifyTitle2(final String html) throws Exception {
1226         return loadPageVerifyTitle2(html, getExpectedAlerts());
1227     }
1228 
1229     protected final WebDriver loadPageVerifyTitle2(final String html, final String... expectedAlerts) throws Exception {
1230         final WebDriver driver = loadPage2(html);
1231         return verifyTitle2(driver, expectedAlerts);
1232     }
1233 
1234     protected final WebDriver verifyTitle2(final Duration maxWaitTime, final WebDriver driver,
1235             final String... expectedAlerts) throws Exception {
1236 
1237         final StringBuilder expected = new StringBuilder();
1238         for (String expectedAlert : expectedAlerts) {
1239             expected.append(expectedAlert).append('\u00A7');
1240         }
1241         final String expectedTitle = expected.toString();
1242 
1243         final long maxWait = System.currentTimeMillis() + maxWaitTime.toMillis();
1244 
1245         while (System.currentTimeMillis() < maxWait) {
1246             try {
1247                 final String title = driver.getTitle();
1248                 assertEquals(expectedTitle, title);
1249                 return driver;
1250             }
1251             catch (final AssertionError e) {
1252                 // ignore and wait
1253             }
1254         }
1255 
1256         assertEquals(expectedTitle, driver.getTitle());
1257         return driver;
1258     }
1259 
1260     protected final WebDriver verifyTitle2(final WebDriver driver,
1261             final String... expectedAlerts) throws Exception {
1262         if (expectedAlerts.length == 0) {
1263             assertEquals("", driver.getTitle());
1264             return driver;
1265         }
1266 
1267         final StringBuilder expected = new StringBuilder();
1268         for (String expectedAlert : expectedAlerts) {
1269             expected.append(expectedAlert).append('\u00A7');
1270         }
1271 
1272         final String title = driver.getTitle();
1273         try {
1274             assertEquals(expected.toString(), title);
1275         }
1276         catch (final AssertionError e) {
1277             if (useRealBrowser() && StringUtils.isEmpty(title)) {
1278                 Thread.sleep(42);
1279                 assertEquals(expected.toString(), driver.getTitle());
1280                 return driver;
1281             }
1282             throw e;
1283         }
1284         return driver;
1285     }
1286 
1287     protected final WebDriver loadPageVerifyTextArea2(final String html) throws Exception {
1288         return loadPageTextArea2(html, getExpectedAlerts());
1289     }
1290 
1291     protected final WebDriver loadPageTextArea2(final String html, final String... expectedAlerts) throws Exception {
1292         final WebDriver driver = loadPage2(html);
1293         return verifyTextArea2(driver, expectedAlerts);
1294     }
1295 
1296     protected final WebDriver verifyTextArea2(final WebDriver driver,
1297             final String... expectedAlerts) throws Exception {
1298         final WebElement textArea = driver.findElement(By.id("myLog"));
1299         String value = textArea.getDomProperty("value");
1300 
1301         if (expectedAlerts.length == 0) {
1302             assertEquals("", value);
1303             return driver;
1304         }
1305 
1306         final StringBuilder expected = new StringBuilder();
1307         for (String expectedAlert : expectedAlerts) {
1308             expected.append(expectedAlert).append('\u00A7');
1309         }
1310 
1311         if (expectedAlerts.length == 1 && expectedAlerts[0].startsWith("data:image/png;base64,")) {
1312             try {
1313                 // fastpath
1314                 assertEquals(expected.toString(), value);
1315             } catch (final AssertionFailedError e) {
1316                 // starting with FF151 we have canvas fingerprinting protection
1317                 // therefore we have to compare the pictures
1318                 if (value.endsWith("\u00A7")) {
1319                     value = value.substring(0, value.length() - 1);
1320                 }
1321 
1322                 compareImages(expectedAlerts[0], value);
1323                 return driver;
1324             }
1325         }
1326 
1327         verify(() -> textArea.getDomProperty("value"), expected.toString());
1328         return driver;
1329     }
1330 
1331     protected final String getJsVariableValue(final WebDriver driver, final String varName) throws Exception {
1332         final String script = "return String(" + varName + ")";
1333         final String result = (String) ((JavascriptExecutor) driver).executeScript(script);
1334 
1335         return result;
1336     }
1337 
1338     protected final WebDriver verifyJsVariable(final WebDriver driver, final String varName,
1339             final String expected) throws Exception {
1340         final String result = getJsVariableValue(driver, varName);
1341         assertEquals(expected, result);
1342 
1343         return driver;
1344     }
1345 
1346     protected final WebDriver verifyWindowName2(final Duration maxWaitTime, final WebDriver driver,
1347             final String... expectedAlerts) throws Exception {
1348         final long maxWait = System.currentTimeMillis() + maxWaitTime.toMillis();
1349 
1350         while (System.currentTimeMillis() < maxWait) {
1351             try {
1352                 return verifyWindowName2(driver, expectedAlerts);
1353             }
1354             catch (final AssertionError e) {
1355                 // ignore and wait
1356             }
1357         }
1358 
1359         return verifyWindowName2(driver, expectedAlerts);
1360     }
1361 
1362     protected final WebDriver verifyWindowName2(final WebDriver driver,
1363             final String... expectedAlerts) throws Exception {
1364         final StringBuilder expected = new StringBuilder();
1365         for (String expectedAlert : expectedAlerts) {
1366             expected.append(expectedAlert).append('\u00A7');
1367         }
1368 
1369         return verifyJsVariable(driver, "window.top.name", expected.toString());
1370     }
1371 
1372     protected final WebDriver verifySessionStorage2(final WebDriver driver,
1373             final String... expectedAlerts) throws Exception {
1374         final StringBuilder expected = new StringBuilder();
1375         for (String expectedAlert : expectedAlerts) {
1376             expected.append(expectedAlert).append('\u00A7');
1377         }
1378 
1379         return verifyJsVariable(driver, "sessionStorage.getItem('Log')", expected.toString());
1380     }
1381 
1382     /**
1383      * Defines the provided HTML as the response for {@link WebTestCase#URL_FIRST}
1384      * and loads the page with this URL using the current WebDriver version; finally, asserts that the
1385      * alerts equal the expected alerts.
1386      * @param html the HTML to use
1387      * @param maxWaitTime the maximum time to wait to get the alerts (in millis)
1388      * @return the web driver
1389      * @throws Exception if something goes wrong
1390      */
1391     protected final WebDriver loadPageWithAlerts2(final String html, final Duration maxWaitTime) throws Exception {
1392         return loadPageWithAlerts2(html, URL_FIRST, maxWaitTime);
1393     }
1394 
1395     /**
1396      * Same as {@link #loadPageWithAlerts2(String)}, but specifying the default URL.
1397      * @param html the HTML to use
1398      * @param url the URL to use to load the page
1399      * @return the web driver
1400      * @throws Exception if something goes wrong
1401      */
1402     protected final WebDriver loadPageWithAlerts2(final String html, final URL url) throws Exception {
1403         return loadPageWithAlerts2(html, url, DEFAULT_WAIT_TIME);
1404     }
1405 
1406     /**
1407      * Same as {@link #loadPageWithAlerts2(String, long)}, but specifying the default URL.
1408      * @param html the HTML to use
1409      * @param url the URL to use to load the page
1410      * @param maxWaitTime the maximum time to wait to get the alerts (in millis)
1411      * @return the web driver
1412      * @throws Exception if something goes wrong
1413      */
1414     protected final WebDriver loadPageWithAlerts2(final String html, final URL url, final Duration maxWaitTime)
1415             throws Exception {
1416         final WebDriver driver = loadPage2(html, url);
1417 
1418         verifyAlerts(maxWaitTime, driver, getExpectedAlerts());
1419         return driver;
1420     }
1421 
1422     /**
1423      * Verifies the captured alerts.
1424      * @param driver the driver instance
1425      * @param expectedAlerts the expected alerts
1426      * @throws Exception in case of failure
1427      */
1428     protected void verifyAlerts(final WebDriver driver, final String... expectedAlerts) throws Exception {
1429         verifyAlerts(DEFAULT_WAIT_TIME, driver, expectedAlerts);
1430     }
1431 
1432     /**
1433      * Verifies the captured alerts.
1434      *
1435      * @param maxWaitTime the maximum time to wait for the expected alert to be found
1436      * @param driver the driver instance
1437      * @param expected the expected alerts
1438      * @throws Exception in case of failure
1439      */
1440     protected void verifyAlerts(final Duration maxWaitTime, final WebDriver driver, final String... expected)
1441             throws Exception {
1442         final List<String> actualAlerts = getCollectedAlerts(maxWaitTime, driver, expected.length);
1443 
1444         assertEquals(expected.length, actualAlerts.size());
1445 
1446         if (!useRealBrowser()) {
1447             // check if we have data-image Url
1448             for (String s : expected) {
1449                 if (s.startsWith("data:image/png;base64,")) {
1450                     // we have to compare element by element
1451                     for (int j = 0; j < expected.length; j++) {
1452                         if (expected[j].startsWith("data:image/png;base64,")) {
1453                             compareImages(expected[j], actualAlerts.get(j));
1454                         } else {
1455                             assertEquals(expected[j], actualAlerts.get(j));
1456                         }
1457                     }
1458                     return;
1459                 }
1460             }
1461         }
1462 
1463         assertEquals(expected, actualAlerts);
1464     }
1465 
1466     /**
1467      * Same as {@link #loadPageWithAlerts2(String)} with additional servlet configuration.
1468      * @param html the HTML to use for the default response
1469      * @param servlets the additional servlets to configure with their mapping
1470      * @return the web driver
1471      * @throws Exception if something goes wrong
1472      */
1473     protected final WebDriver loadPageWithAlerts2(final String html,
1474             final Map<String, Class<? extends Servlet>> servlets) throws Exception {
1475         return loadPageWithAlerts2(html, URL_FIRST, DEFAULT_WAIT_TIME, servlets);
1476     }
1477 
1478     /**
1479      * Same as {@link #loadPageWithAlerts2(String, URL, long)}, but with additional servlet configuration.
1480      * @param html the HTML to use for the default page
1481      * @param url the URL to use to load the page
1482      * @param maxWaitTime the maximum time to wait to get the alerts (in millis)
1483      * @param servlets the additional servlets to configure with their mapping
1484      * @return the web driver
1485      * @throws Exception if something goes wrong
1486      */
1487     protected final WebDriver loadPageWithAlerts2(final String html, final URL url, final Duration maxWaitTime,
1488             final Map<String, Class<? extends Servlet>> servlets) throws Exception {
1489 
1490         expandExpectedAlertsVariables(URL_FIRST);
1491 
1492         final WebDriver driver = loadPage2(html, url, servlets);
1493         verifyAlerts(maxWaitTime, driver, getExpectedAlerts());
1494 
1495         return driver;
1496     }
1497 
1498     /**
1499      * Loads the provided URL serving responses from {@link #getMockWebConnection()}
1500      * and verifies that the captured alerts are correct.
1501      * @param url the URL to use to load the page
1502      * @return the web driver
1503      * @throws Exception if something goes wrong
1504      */
1505     protected final WebDriver loadPageWithAlerts2(final URL url) throws Exception {
1506         return loadPageWithAlerts2(url, DEFAULT_WAIT_TIME);
1507     }
1508 
1509     /**
1510      * Same as {@link #loadPageWithAlerts2(URL)}, but using with timeout.
1511      * @param url the URL to use to load the page
1512      * @param maxWaitTime the maximum time to wait to get the alerts (in millis)
1513      * @return the web driver
1514      * @throws Exception if something goes wrong
1515      */
1516     protected final WebDriver loadPageWithAlerts2(final URL url, final Duration maxWaitTime) throws Exception {
1517         startWebServer(getMockWebConnection(), null);
1518 
1519         final WebDriver driver = getWebDriver();
1520         driver.get(url.toExternalForm());
1521 
1522         verifyAlerts(maxWaitTime, driver, getExpectedAlerts());
1523         return driver;
1524     }
1525 
1526     /**
1527      * Gets the alerts collected by the driver.
1528      * Note: it currently works only if no new page has been loaded in the window
1529      * @param driver the driver
1530      * @return the collected alerts
1531      * @throws Exception in case of problem
1532      */
1533     protected List<String> getCollectedAlerts(final WebDriver driver) throws Exception {
1534         return getCollectedAlerts(driver, getExpectedAlerts().length);
1535     }
1536 
1537     /**
1538      * Gets the alerts collected by the driver.
1539      * Note: it currently works only if no new page has been loaded in the window
1540      * @param driver the driver
1541      * @param alertsLength the expected length of Alerts
1542      * @return the collected alerts
1543      * @throws Exception in case of problem
1544      */
1545     protected List<String> getCollectedAlerts(final WebDriver driver, final int alertsLength) throws Exception {
1546         return getCollectedAlerts(DEFAULT_WAIT_TIME, driver, alertsLength);
1547     }
1548 
1549     /**
1550      * Gets the alerts collected by the driver.
1551      * Note: it currently works only if no new page has been loaded in the window
1552      * @param maxWaitTime the maximum time to wait to get the alerts (in millis)
1553      * @param driver the driver
1554      * @param alertsLength the expected length of Alerts
1555      * @return the collected alerts
1556      * @throws Exception in case of problem
1557      */
1558     protected List<String> getCollectedAlerts(final Duration maxWaitTime,
1559             final WebDriver driver, final int alertsLength) throws Exception {
1560         final List<String> collectedAlerts = new ArrayList<>();
1561 
1562         long maxWait = System.currentTimeMillis() + maxWaitTime.toMillis();
1563 
1564         while (collectedAlerts.size() < alertsLength && System.currentTimeMillis() < maxWait) {
1565             try {
1566                 final Alert alert = driver.switchTo().alert();
1567                 final String text = alert.getText();
1568 
1569                 collectedAlerts.add(text);
1570                 alert.accept();
1571 
1572                 // handling of alerts requires some time
1573                 // at least for tests with many alerts we have to take this into account
1574                 maxWait += 100;
1575             }
1576             catch (final NoAlertPresentException e) {
1577                 Thread.sleep(10);
1578             }
1579         }
1580 
1581         return collectedAlerts;
1582     }
1583 
1584     /**
1585      * Returns the HtmlElement of the specified WebElement.
1586      * @param webElement the webElement
1587      * @return the HtmlElement
1588      * @throws Exception if an error occurs
1589      */
1590     protected HtmlElement toHtmlElement(final WebElement webElement) throws Exception {
1591         return (HtmlElement) ((HtmlUnitWebElement) webElement).getElement();
1592     }
1593 
1594     /**
1595      * Loads an expectation file for the specified browser search first for a browser specific resource
1596      * and falling back in a general resource.
1597      * @param resourcePrefix the start of the resource name
1598      * @param resourceSuffix the end of the resource name
1599      * @return the content of the file
1600      * @throws Exception in case of error
1601      */
1602     protected String loadExpectation(final String resourcePrefix, final String resourceSuffix) throws Exception {
1603         final Class<?> referenceClass = getClass();
1604         final BrowserVersion browserVersion = getBrowserVersion();
1605 
1606         String realBrowserNyiExpectation = null;
1607         String realNyiExpectation = null;
1608         final String browserExpectation;
1609         final String expectation;
1610         if (!useRealBrowser()) {
1611             // first try nyi
1612             final String browserSpecificNyiResource
1613                     = resourcePrefix + "." + browserVersion.getNickname() + "_NYI" + resourceSuffix;
1614             realBrowserNyiExpectation = loadContent(referenceClass.getResource(browserSpecificNyiResource));
1615 
1616             // next nyi without browser
1617             final String nyiResource = resourcePrefix + ".NYI" + resourceSuffix;
1618             realNyiExpectation = loadContent(referenceClass.getResource(nyiResource));
1619         }
1620 
1621         // implemented - browser specific
1622         final String browserSpecificResource = resourcePrefix + "." + browserVersion.getNickname() + resourceSuffix;
1623         browserExpectation = loadContent(referenceClass.getResource(browserSpecificResource));
1624 
1625         // implemented - all browsers
1626         final String resource = resourcePrefix + resourceSuffix;
1627         expectation = loadContent(referenceClass.getResource(resource));
1628 
1629         // check for duplicates
1630         if (realBrowserNyiExpectation != null) {
1631             if (realNyiExpectation != null) {
1632                 Assertions.assertNotEquals(realBrowserNyiExpectation, realNyiExpectation,
1633                         "Duplicate NYI Expectation for Browser " + browserVersion.getNickname());
1634             }
1635 
1636             if (browserExpectation == null) {
1637                 if (expectation != null) {
1638                     Assertions.assertNotEquals(realBrowserNyiExpectation, expectation,
1639                             "NYI Expectation matches the expected "
1640                                     + "result for Browser " + browserVersion.getNickname());
1641                 }
1642             }
1643             else {
1644                 Assertions.assertNotEquals(realBrowserNyiExpectation, browserExpectation,
1645                         "NYI Expectation matches the expected "
1646                                 + "browser specific result for Browser "
1647                                 + browserVersion.getNickname());
1648             }
1649 
1650             return realBrowserNyiExpectation;
1651         }
1652 
1653         if (realNyiExpectation != null) {
1654             if (browserExpectation == null) {
1655                 if (expectation != null) {
1656                     Assertions.assertNotEquals(realNyiExpectation, expectation,
1657                             "NYI Expectation matches the expected "
1658                                     + "result for Browser " + browserVersion.getNickname());
1659                 }
1660             }
1661             else {
1662                 Assertions.assertNotEquals(realNyiExpectation, browserExpectation,
1663                             "NYI Expectation matches the expected "
1664                                     + "browser specific result for Browser "
1665                                     + browserVersion.getNickname());
1666             }
1667             return realNyiExpectation;
1668         }
1669 
1670         if (browserExpectation != null) {
1671             if (expectation != null) {
1672                 Assertions.assertNotEquals(browserExpectation, expectation,
1673                             "Browser specific NYI Expectation matches the expected "
1674                                     + "result for Browser " + browserVersion.getNickname());
1675             }
1676             return browserExpectation;
1677         }
1678         return expectation;
1679     }
1680 
1681     private static String loadContent(final URL url) throws URISyntaxException, IOException {
1682         if (url == null) {
1683             return null;
1684         }
1685 
1686         final File file = new File(url.toURI());
1687         final String content = FileUtils.readFileToString(file, UTF_8)
1688                                         .replace("\r\n", "\n");
1689         return content;
1690     }
1691 
1692     /**
1693      * Reads the number of JS threads remaining from unit tests run before.
1694      * This should be always 0, if {@link #isWebClientCached()} is {@code false}.
1695      * @throws InterruptedException in case of problems
1696      */
1697     @BeforeEach
1698     public void beforeTest() throws InterruptedException {
1699         if (needDownloads()) {
1700             clearDownloads();
1701         }
1702         if (!isWebClientCached()) {
1703             if (!getJavaScriptThreads().isEmpty()) {
1704                 Thread.sleep(200);
1705             }
1706             assertTrue("There are already JS threads running before the test", getJavaScriptThreads().isEmpty());
1707         }
1708     }
1709 
1710     /**
1711      * Asserts the current title is equal to the expectation string.
1712      * @param webdriver the driver in use
1713      * @param expected the expected object
1714      * @throws Exception in case of failure
1715      */
1716     protected void assertTitle(final WebDriver webdriver, final String expected) throws Exception {
1717         final long maxWait = System.currentTimeMillis() + DEFAULT_WAIT_TIME.toMillis();
1718 
1719         while (true) {
1720             final String title = webdriver.getTitle();
1721             try {
1722                 assertEquals(expected, title);
1723                 return;
1724             }
1725             catch (final org.opentest4j.AssertionFailedError e) {
1726                 if (expected.length() <= title.length()
1727                         || System.currentTimeMillis() > maxWait) {
1728                     throw e;
1729                 }
1730                 Thread.sleep(10);
1731             }
1732         }
1733     }
1734 
1735     /**
1736      * Release resources but DON'T close the browser if we are running with a real browser.
1737      * Note that HtmlUnitDriver is not cached by default, but that can be configured by {@link #isWebClientCached()}.
1738      */
1739     @AfterEach
1740     @Override
1741     public void releaseResources() {
1742         final List<String> unhandledAlerts = new ArrayList<>();
1743         if (webDriver_ != null) {
1744             UnhandledAlertException ex = null;
1745             do {
1746                 ex = null;
1747                 try {
1748                     // getTitle will do an implicit check for open alerts
1749                     webDriver_.getTitle();
1750                 }
1751                 catch (final NoSuchWindowException e) {
1752                     // ignore
1753                 }
1754                 catch (final UnhandledAlertException e) {
1755                     ex = e;
1756                     unhandledAlerts.add(e.getMessage());
1757                 }
1758             }
1759             while (ex != null);
1760         }
1761 
1762         super.releaseResources();
1763 
1764         if (!isWebClientCached()) {
1765             boolean rhino = false;
1766             if (webDriver_ != null) {
1767                 try {
1768                     rhino = getWebClient().getJavaScriptEngine() instanceof JavaScriptEngine;
1769                 }
1770                 catch (final Exception e) {
1771                     throw new RuntimeException(e);
1772                 }
1773                 webDriver_.quit();
1774                 webDriver_ = null;
1775             }
1776             if (rhino) {
1777                 assertTrue("There are still JS threads running after the test", getJavaScriptThreads().isEmpty());
1778             }
1779         }
1780 
1781         if (useRealBrowser()) {
1782             synchronized (WEB_DRIVERS_REAL_BROWSERS) {
1783                 final RealBrowserKey realBrowserKey = realBrowserKey();
1784                 final WebDriver driver = WEB_DRIVERS_REAL_BROWSERS.get(realBrowserKey);
1785                 if (driver != null) {
1786                     try {
1787                         final String currentWindow = driver.getWindowHandle();
1788 
1789                         final Set<String> handles = driver.getWindowHandles();
1790                         // close all windows except the current one
1791                         handles.remove(currentWindow);
1792                         if (needDownloads()) {
1793                             // some browsers (edge) open an extra window/tab for a download and
1794                             // need 20s to close it (or fail); leave it open, the browser is
1795                             // quit at the end anyway
1796                             handles.clear();
1797                         }
1798 
1799                         if (!handles.isEmpty()) {
1800                             for (final String handle : handles) {
1801                                 try {
1802                                     driver.switchTo().window(handle);
1803                                     driver.close();
1804                                 }
1805                                 catch (final NoSuchWindowException e) {
1806                                     LOG.error("Error switching to browser window; quit browser.", e);
1807                                     WEB_DRIVERS_REAL_BROWSERS.remove(realBrowserKey);
1808                                     WEB_DRIVERS_REAL_BROWSERS_USAGE_COUNT.remove(realBrowserKey);
1809                                     driver.quit();
1810                                     return;
1811                                 }
1812                             }
1813 
1814                             // we have to force WebDriver to treat the remaining window
1815                             // as the one we like to work with from now on
1816                             // looks like a web driver issue to me (version 2.47.2)
1817                             driver.switchTo().window(currentWindow);
1818                         }
1819 
1820                         driver.manage().deleteAllCookies();
1821 
1822                         // in the remaining window, load a blank page
1823                         driver.get("about:blank");
1824                     }
1825                     catch (final NoSuchSessionException e) {
1826                         LOG.error("Error browser session no longer available.", e);
1827                         WEB_DRIVERS_REAL_BROWSERS.remove(realBrowserKey);
1828                         WEB_DRIVERS_REAL_BROWSERS_USAGE_COUNT.remove(realBrowserKey);
1829                         return;
1830                     }
1831                     catch (final WebDriverException e) {
1832                         shutDownRealBrowsers();
1833                     }
1834                 }
1835             }
1836         }
1837         assertTrue("There are still unhandled alerts: " + String.join("; ", unhandledAlerts),
1838                         unhandledAlerts.isEmpty());
1839     }
1840 
1841     /**
1842      * Returns the underlying WebWindow of the specified driver.
1843      *
1844      * <b>Your test shouldn't depend primarily on WebClient</b>
1845      *
1846      * @return the current web window
1847      * @see #toHtmlElement(WebElement)
1848      */
1849     protected WebWindow getWebWindow() {
1850         return webDriver_.getCurrentWindow().getWebWindow();
1851     }
1852 
1853     /**
1854      * Whether {@link WebClient} is cached or not, defaults to {@code false}.
1855      *
1856      * <p>This is needed to be {@code true} for huge test class, as we could run out of sockets.
1857      *
1858      * @return whether {@link WebClient} is cached or not
1859      */
1860     protected boolean isWebClientCached() {
1861         return false;
1862     }
1863 
1864     /**
1865      * Configure {@link WebClientOptions#getTimeout()}.
1866      *
1867      * @return null if unchanged otherwise the timeout as int
1868      */
1869     protected Integer getWebClientTimeout() {
1870         return null;
1871     }
1872 
1873     protected Page getEnclosedPage() {
1874         return getWebWindow().getEnclosedPage();
1875     }
1876 
1877     protected WebClient getWebClient() {
1878         return webDriver_.getWebClient();
1879     }
1880 }