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.jupiter.api.AfterEach;
27 import org.junit.jupiter.api.BeforeEach;
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 Mike Bowler
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 * @throws Exception in case of error
233 */
234 @BeforeEach
235 public void before() throws Exception {
236 if (webClient_ != null && webClient_.getJavaScriptEngine() instanceof JavaScriptEngine) {
237 if (!getJavaScriptThreads().isEmpty()) {
238 Thread.sleep(200);
239 }
240 assertTrue("There are already JS threads running before the test", getJavaScriptThreads().isEmpty());
241 }
242 }
243
244 /**
245 * Cleanup after a test.
246 */
247 @Override
248 @AfterEach
249 public void releaseResources() {
250 super.releaseResources();
251 boolean rhino = false;
252 if (webClient_ != null) {
253 rhino = webClient_.getJavaScriptEngine() instanceof JavaScriptEngine;
254 webClient_.close();
255 webClient_.getCookieManager().clearCookies();
256 }
257 webClient_ = null;
258
259 if (rhino) {
260 final List<Thread> jsThreads = getJavaScriptThreads();
261 assertEquals(0, jsThreads.size());
262
263 // collect stack traces
264 // caution: the threads may terminate after the threads have been returned by getJavaScriptThreads()
265 // and before stack traces are retrieved
266 if (jsThreads.size() > 0) {
267 final Map<String, StackTraceElement[]> stackTraces = new HashMap<>();
268 for (final Thread t : jsThreads) {
269 final StackTraceElement[] elts = t.getStackTrace();
270 if (elts != null) {
271 stackTraces.put(t.getName(), elts);
272 }
273 }
274
275 if (!stackTraces.isEmpty()) {
276 System.err.println("JS threads still running:");
277 for (final Map.Entry<String, StackTraceElement[]> entry : stackTraces.entrySet()) {
278 System.err.println("Thread: " + entry.getKey());
279 final StackTraceElement[] elts = entry.getValue();
280 for (final StackTraceElement elt : elts) {
281 System.err.println(elt);
282 }
283 }
284 throw new RuntimeException("JS threads are still running: " + jsThreads.size());
285 }
286 }
287 }
288 }
289 }