View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import java.net.URL;
18  import java.time.Duration;
19  import java.util.ArrayList;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import org.htmlunit.html.HtmlPage;
25  import org.htmlunit.javascript.JavaScriptEngine;
26  import org.junit.After;
27  import org.junit.Before;
28  
29  /**
30   * A simple WebTestCase which doesn't require server to run, and doens't use WebDriver.
31   *
32   * It depends on {@link MockWebConnection} to simulate sending requests to the server.
33   *
34   * <b>Note that {@link WebDriverTestCase} should be used unless HtmlUnit-specific feature
35   * is needed and Selenium does not support it.</b>
36   *
37   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
38   * @author David D. Kilzer
39   * @author Marc Guillemot
40   * @author Chris Erskine
41   * @author Michael Ottati
42   * @author Daniel Gredler
43   * @author Ahmed Ashour
44   * @author Ronald Brill
45   */
46  public abstract class SimpleWebTestCase extends WebTestCase {
47  
48      private WebClient webClient_;
49  
50      /**
51       * Load a page with the specified HTML using the default browser version.
52       * @param html the HTML to use
53       * @return the new page
54       * @throws Exception if something goes wrong
55       */
56      public final HtmlPage loadPage(final String html) throws Exception {
57          return loadPage(html, null);
58      }
59  
60      /**
61       * User the default browser version to load a page with the specified HTML
62       * and collect alerts into the list.
63       * @param html the HTML to use
64       * @param collectedAlerts the list to hold the alerts
65       * @return the new page
66       * @throws Exception if something goes wrong
67       */
68      public final HtmlPage loadPage(final String html, final List<String> collectedAlerts) throws Exception {
69          return loadPage(getBrowserVersion(), html, collectedAlerts, URL_FIRST);
70      }
71  
72      /**
73       * Loads a page with the specified HTML and collect alerts into the list.
74       * @param html the HTML to use
75       * @param collectedAlerts the list to hold the alerts
76       * @param url the URL that will use as the document host for this page
77       * @return the new page
78       * @throws Exception if something goes wrong
79       */
80      protected final HtmlPage loadPage(final String html, final List<String> collectedAlerts,
81              final URL url) throws Exception {
82  
83          return loadPage(BrowserVersion.getDefault(), html, collectedAlerts, url);
84      }
85  
86      /**
87       * Load a page with the specified HTML and collect alerts into the list.
88       * @param browserVersion the browser version to use
89       * @param html the HTML to use
90       * @param collectedAlerts the list to hold the alerts
91       * @param url the URL that will use as the document host for this page
92       * @return the new page
93       * @throws Exception if something goes wrong
94       */
95      protected final HtmlPage loadPage(final BrowserVersion browserVersion,
96              final String html, final List<String> collectedAlerts, final URL url) throws Exception {
97  
98          if (webClient_ == null) {
99              webClient_ = new WebClient(browserVersion);
100         }
101         return loadPage(webClient_, html, collectedAlerts, url);
102     }
103 
104     /**
105      * Load a page with the specified HTML and collect alerts into the list.
106      * @param client the WebClient to use (webConnection and alertHandler will be configured on it)
107      * @param html the HTML to use
108      * @param collectedAlerts the list to hold the alerts
109      * @param url the URL that will use as the document host for this page
110      * @return the new page
111      * @throws Exception if something goes wrong
112      */
113     protected final HtmlPage loadPage(final WebClient client,
114             final String html, final List<String> collectedAlerts, final URL url) throws Exception {
115 
116         if (collectedAlerts != null) {
117             client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
118         }
119 
120         final MockWebConnection webConnection = getMockWebConnection();
121         webConnection.setDefaultResponse(html);
122         client.setWebConnection(webConnection);
123 
124         return client.getPage(url);
125     }
126 
127     /**
128      * Load a page with the specified HTML and collect alerts into the list.
129      * @param client the WebClient to use (webConnection and alertHandler will be configured on it)
130      * @param html the HTML to use
131      * @param collectedAlerts the list to hold the alerts
132      * @return the new page
133      * @throws Exception if something goes wrong
134      */
135     protected final HtmlPage loadPage(final WebClient client,
136             final String html, final List<String> collectedAlerts) throws Exception {
137 
138         return loadPage(client, html, collectedAlerts, URL_FIRST);
139     }
140 
141     /**
142      * Convenience method to pull the MockWebConnection out of an HtmlPage created with
143      * the loadPage method.
144      * @param page HtmlPage to get the connection from
145      * @return the MockWebConnection that served this page
146      */
147     protected static final MockWebConnection getMockConnection(final HtmlPage page) {
148         return (MockWebConnection) page.getWebClient().getWebConnection();
149     }
150 
151     /**
152      * Returns the WebClient instance for the current test with the current {@link BrowserVersion}.
153      * @return a WebClient with the current {@link BrowserVersion}
154      */
155     protected WebClient createNewWebClient() {
156         final WebClient webClient = new WebClient(getBrowserVersion());
157         return webClient;
158     }
159 
160     /**
161      * Returns the WebClient instance for the current test with the current {@link BrowserVersion}.
162      * @return a WebClient with the current {@link BrowserVersion}
163      */
164     protected final WebClient getWebClient() {
165         if (webClient_ == null) {
166             webClient_ = createNewWebClient();
167         }
168         return webClient_;
169     }
170 
171     /**
172      * Returns the WebClient instance for the current test with the current {@link BrowserVersion}.
173      * @return a WebClient with the current {@link BrowserVersion}
174      */
175     protected final WebClient getWebClientWithMockWebConnection() {
176         if (webClient_ == null) {
177             webClient_ = createNewWebClient();
178             webClient_.setWebConnection(getMockWebConnection());
179         }
180         return webClient_;
181     }
182 
183     /**
184      * Defines the provided HTML as the response of the MockWebConnection for {@link WebTestCase#URL_FIRST}
185      * and loads the page with this URL using the current browser version; finally, asserts that the
186      * alerts equal the expected alerts.
187      * @param html the HTML to use
188      * @return the new page
189      * @throws Exception if something goes wrong
190      */
191     protected final HtmlPage loadPageWithAlerts(final String html) throws Exception {
192         return loadPageWithAlerts(html, URL_FIRST, null);
193     }
194 
195     /**
196      * Defines the provided HTML as the response of the MockWebConnection for {@link WebTestCase#URL_FIRST}
197      * and loads the page with this URL using the current browser version; finally, asserts the alerts
198      * equal the expected alerts.
199      * @param html the HTML to use
200      * @param url the URL from which the provided HTML code should be delivered
201      * @param waitForJS the milliseconds to wait for background JS tasks to complete. Ignored if -1.
202      * @return the new page
203      * @throws Exception if something goes wrong
204      */
205     protected final HtmlPage loadPageWithAlerts(final String html, final URL url, final Duration waitForJS)
206         throws Exception {
207         if (getExpectedAlerts() == null) {
208             throw new IllegalStateException("You must annotate the test class with '@RunWith(BrowserRunner.class)'");
209         }
210 
211         // expand variables in expected alerts
212         expandExpectedAlertsVariables(url);
213 
214         final WebClient client = getWebClientWithMockWebConnection();
215         final List<String> collectedAlerts = new ArrayList<>();
216         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
217 
218         final MockWebConnection webConnection = getMockWebConnection();
219         webConnection.setResponse(url, html);
220 
221         final HtmlPage page = client.getPage(url);
222         if (waitForJS != null) {
223             assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(waitForJS.toMillis()));
224         }
225         assertEquals(getExpectedAlerts(), collectedAlerts);
226         return page;
227     }
228 
229     /**
230      * Reads the number of JS threads remaining from unit tests run before.
231      * This should be always 0.
232      */
233     @Before
234     public void before() {
235         if (webClient_ != null && webClient_.getJavaScriptEngine() instanceof JavaScriptEngine) {
236             assertTrue(getJavaScriptThreads().isEmpty());
237         }
238     }
239 
240     /**
241      * Cleanup after a test.
242      */
243     @Override
244     @After
245     public void releaseResources() {
246         super.releaseResources();
247         boolean rhino = false;
248         if (webClient_ != null) {
249             rhino = webClient_.getJavaScriptEngine() instanceof JavaScriptEngine;
250             webClient_.close();
251             webClient_.getCookieManager().clearCookies();
252         }
253         webClient_ = null;
254 
255         if (rhino) {
256             final List<Thread> jsThreads = getJavaScriptThreads();
257             assertEquals(0, jsThreads.size());
258 
259             // collect stack traces
260             // caution: the threads may terminate after the threads have been returned by getJavaScriptThreads()
261             // and before stack traces are retrieved
262             if (jsThreads.size() > 0) {
263                 final Map<String, StackTraceElement[]> stackTraces = new HashMap<>();
264                 for (final Thread t : jsThreads) {
265                     final StackTraceElement[] elts = t.getStackTrace();
266                     if (elts != null) {
267                         stackTraces.put(t.getName(), elts);
268                     }
269                 }
270 
271                 if (!stackTraces.isEmpty()) {
272                     System.err.println("JS threads still running:");
273                     for (final Map.Entry<String, StackTraceElement[]> entry : stackTraces.entrySet()) {
274                         System.err.println("Thread: " + entry.getKey());
275                         final StackTraceElement[] elts = entry.getValue();
276                         for (final StackTraceElement elt : elts) {
277                             System.err.println(elt);
278                         }
279                     }
280                     throw new RuntimeException("JS threads are still running: " + jsThreads.size());
281                 }
282             }
283         }
284     }
285 }