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.javascript;
16  
17  import static org.junit.Assert.fail;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.IOException;
21  import java.lang.reflect.Field;
22  import java.net.URL;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.zip.GZIPOutputStream;
27  
28  import org.htmlunit.BrowserVersion;
29  import org.htmlunit.CollectingAlertHandler;
30  import org.htmlunit.FailingHttpStatusCodeException;
31  import org.htmlunit.HttpHeader;
32  import org.htmlunit.HttpMethod;
33  import org.htmlunit.MockWebConnection;
34  import org.htmlunit.ScriptException;
35  import org.htmlunit.SimpleWebTestCase;
36  import org.htmlunit.WebClient;
37  import org.htmlunit.WebRequest;
38  import org.htmlunit.WebWindow;
39  import org.htmlunit.corejs.javascript.Context;
40  import org.htmlunit.corejs.javascript.ContextFactory;
41  import org.htmlunit.corejs.javascript.Function;
42  import org.htmlunit.corejs.javascript.Script;
43  import org.htmlunit.corejs.javascript.Scriptable;
44  import org.htmlunit.html.DomNode;
45  import org.htmlunit.html.HtmlButtonInput;
46  import org.htmlunit.html.HtmlElement;
47  import org.htmlunit.html.HtmlForm;
48  import org.htmlunit.html.HtmlFrame;
49  import org.htmlunit.html.HtmlPage;
50  import org.htmlunit.html.HtmlScript;
51  import org.htmlunit.html.HtmlTextInput;
52  import org.htmlunit.junit.BrowserRunner;
53  import org.htmlunit.junit.annotation.Alerts;
54  import org.htmlunit.junit.annotation.Retry;
55  import org.htmlunit.util.NameValuePair;
56  import org.htmlunit.util.UrlUtils;
57  import org.junit.Test;
58  import org.junit.runner.RunWith;
59  
60  /**
61   * Tests for the {@link JavaScriptEngine}.
62   *
63   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
64   * @author Noboru Sinohara
65   * @author Darrell DeBoer
66   * @author <a href="mailto:bcurren@esomnie.com">Ben Curren</a>
67   * @author Marc Guillemot
68   * @author Chris Erskine
69   * @author David K. Taylor
70   * @author Ahmed Ashour
71   * @author Ronald Brill
72   * @author Carsten Steul
73   */
74  @RunWith(BrowserRunner.class)
75  public class JavaScriptEngineTest extends SimpleWebTestCase {
76  
77      /**
78       * @throws Exception if the test fails
79       */
80      @Test
81      public void setJavascriptEnabled_false() throws Exception {
82          final String html = DOCTYPE_HTML
83              + "<html><head><title>foo</title><script>\n"
84              + "  document.form1.textfield1 = 'blue'"
85              + "</script></head><body>\n"
86              + "<p>hello world</p>\n"
87              + "<form name='form1'>\n"
88              + "  <input type='text' name='textfield1' id='textfield1' value='foo' />\n"
89              + "  <input type='text' name='textfield2' id='textfield2'/>\n"
90              + "</form>\n"
91              + "</body></html>";
92  
93          getWebClientWithMockWebConnection().getOptions().setJavaScriptEnabled(false);
94  
95          final HtmlPage page = loadPageWithAlerts(html);
96  
97          final HtmlTextInput textInput = page.getHtmlElementById("textfield1");
98          assertEquals("foo", textInput.getValueAttribute());
99          assertEquals("foo", textInput.getValue());
100     }
101 
102     /**
103      * Tries to set the value of a text input field.
104      * @throws Exception if the test fails
105      */
106     @Test
107     public void setInputValue() throws Exception {
108         final String content = DOCTYPE_HTML
109             + "<html><head><title>foo</title><script>\n"
110             + "function doTest() {\n"
111             + "  document.form1.textfield1.value = 'blue'"
112             + "}\n"
113             + "</script></head><body onload='doTest()'>\n"
114             + "<p>hello world</p>\n"
115             + "<form name='form1'>\n"
116             + "  <input type='text' name='textfield1' id='textfield1' value='foo' />\n"
117             + "  <input type='text' name='textfield2' id='textfield2'/>\n"
118             + "</form>\n"
119             + "</body></html>";
120         final List<String> collectedAlerts = null;
121         final HtmlPage page = loadPage(content, collectedAlerts);
122 
123         final HtmlTextInput textInput = page.getHtmlElementById("textfield1");
124         assertEquals("foo", textInput.getValueAttribute());
125         assertEquals("blue", textInput.getValue());
126     }
127 
128     /**
129      * Checks that a dynamically compiled function works in the scope of its birth and not the other window.
130      * @throws Exception if the test fails
131      */
132     @Test
133     public void scopeOfNewFunctionCalledFormOtherWindow() throws Exception {
134         final String firstContent = DOCTYPE_HTML
135             + "<html><head>\n"
136             + "<script>\n"
137             + "var foo = 'foo';\n"
138             + "var test = new Function('alert(foo);');\n"
139             + "</script>\n"
140             + "</head>\n"
141             + "<body onload='test()'>\n"
142             + "  <iframe src='page2.html'/>\n"
143             + "</body>\n"
144             + "</html>";
145 
146         final String secondContent = DOCTYPE_HTML
147             + "<html><head><script>\n"
148             + "var foo = 'foo2';\n"
149             + "parent.test();\n"
150             + "var f = parent.test;\n"
151             + "f();\n"
152             + "</script></head></html>";
153 
154         final WebClient client = getWebClient();
155         final MockWebConnection webConnection = new MockWebConnection();
156         webConnection.setDefaultResponse(secondContent);
157         webConnection.setResponse(URL_FIRST, firstContent);
158         client.setWebConnection(webConnection);
159 
160         final String[] expectedAlerts = {"foo", "foo", "foo"};
161 
162         final List<String> collectedAlerts = new ArrayList<>();
163         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
164         client.getPage(URL_FIRST);
165 
166         assertEquals(expectedAlerts, collectedAlerts);
167     }
168 
169     /**
170      * If a reference has been hold on a page and the page is not
171      * anymore the one contained in "its" window, JavaScript should not be executed.
172      * @throws Exception if the test fails
173      */
174     @Test
175     public void scopeInInactivePage() throws Exception {
176         final String firstContent = DOCTYPE_HTML
177             + "<html><head>\n"
178             + "<script>\n"
179             + "var foo = 'foo';\n"
180             + "</script>\n"
181             + "</head>\n"
182             + "<body>\n"
183             + "  <a href='page2.html'>to page 2</a>\n"
184             + "  <div id='testdiv' onclick='alert(foo)'>foo</div>\n"
185             + "</body>\n"
186             + "</html>";
187 
188         final WebClient client = getWebClient();
189         final MockWebConnection webConnection = new MockWebConnection();
190         webConnection.setDefaultResponse("<html></html>");
191         webConnection.setResponse(URL_FIRST, firstContent);
192         client.setWebConnection(webConnection);
193 
194         final String[] expectedAlerts = {"foo"};
195 
196         final List<String> collectedAlerts = new ArrayList<>();
197         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
198         final HtmlPage page = client.getPage(URL_FIRST);
199         final HtmlElement div = page.getHtmlElementById("testdiv");
200 
201         div.click();
202         assertEquals(expectedAlerts, collectedAlerts);
203         collectedAlerts.clear();
204 
205         page.getAnchors().get(0).click();
206 
207         // ignore response, and click in the page again that is not "active" anymore
208         div.click();
209         assertEquals(Collections.emptyList(), collectedAlerts);
210     }
211 
212     /**
213      * @throws Exception if the test fails
214      */
215     @Test
216     @Alerts("got here")
217     public void externalScript() throws Exception {
218         final String html = DOCTYPE_HTML
219             + "<html><head><title>foo</title><script src='/foo.js' id='script1'/>\n"
220             + "</head><body>\n"
221             + "<p>hello world</p>\n"
222             + "<form name='form1'>\n"
223             + "  <input type='text' name='textfield1' id='textfield1' value='foo' />\n"
224             + "  <input type='text' name='textfield2' id='textfield2'/>\n"
225             + "</form>\n"
226             + "</body></html>";
227 
228         final String jsContent = "alert('got here');\n";
229 
230         getMockWebConnection().setResponse(new URL(URL_FIRST, "foo.js"), jsContent,
231                 "text/javascript");
232 
233         final HtmlPage page = loadPageWithAlerts(html);
234         final HtmlScript htmlScript = page.getHtmlElementById("script1");
235         assertNotNull(htmlScript);
236     }
237 
238     /**
239      * An odd case, if two external scripts are referenced and the &lt;script&gt; element
240      * of the first contain a comment which contain an apostrophe, then the second script
241      * is ignored. Bug #632.
242      * @throws Exception if the test fails
243      */
244     @Test
245     public void externalScriptWithApostrophesInComment() throws Exception {
246         final WebClient client = getWebClient();
247         final MockWebConnection webConnection = new MockWebConnection();
248 
249         final String htmlContent = DOCTYPE_HTML
250             + "<html><head><title>foo</title>\n"
251             + "<script src='/foo.js' id='script1'><!-- this shouldn't be a problem --></script>\n"
252             + "<script src='/foo2.js' id='script2'><!-- this shouldn't be a problem --></script>\n"
253             + "</head><body>\n"
254             + "<p>hello world</p>\n"
255             + "</body></html>";
256 
257         webConnection.setResponse(URL_FIRST, htmlContent);
258         webConnection.setResponse(new URL(URL_FIRST, "foo.js"), "alert('got here');", "text/javascript");
259         webConnection.setResponse(new URL(URL_FIRST, "foo2.js"), "alert('got here 2');", "text/javascript");
260         client.setWebConnection(webConnection);
261 
262         final String[] expectedAlerts = {"got here", "got here 2"};
263         final List<String> collectedAlerts = new ArrayList<>();
264         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
265 
266         final HtmlPage page = client.getPage(URL_FIRST);
267         assertEquals(expectedAlerts, collectedAlerts);
268 
269         assertNotNull(page.getHtmlElementById("script1"));
270         assertNotNull(page.getHtmlElementById("script2"));
271     }
272 
273     /**
274      * Test that the URL of the page containing the script is contained in the exception's message.
275      * @throws Exception if the test fails
276      */
277     @Test
278     public void scriptErrorContainsPageUrl() throws Exception {
279         // embedded script
280         final String content1 = DOCTYPE_HTML
281             + "<html><head><script>a.foo</script>\n"
282             + "</head><body>\n"
283             + "</body></html>";
284 
285         try {
286             loadPageWithAlerts(content1);
287         }
288         catch (final Exception e) {
289             assertTrue(e.getMessage().indexOf(URL_FIRST.toString()) > -1);
290         }
291 
292         // external script
293         final WebClient client = getWebClient();
294         final MockWebConnection webConnection = new MockWebConnection();
295 
296         final String content2 = DOCTYPE_HTML
297             + "<html><head><title>foo</title><script src='/foo.js'/>\n"
298             + "</head><body>\n"
299             + "</body></html>";
300 
301         final String jsContent = "a.foo = 213;\n";
302 
303         webConnection.setResponse(URL_FIRST, content2);
304         final URL urlScript = new URL(URL_FIRST, "foo.js");
305         webConnection.setResponse(urlScript, jsContent, "text/javascript");
306         client.setWebConnection(webConnection);
307 
308         try {
309             client.getPage(URL_FIRST);
310         }
311         catch (final Exception e) {
312             assertTrue(e.getMessage(), e.getMessage().indexOf(urlScript.toString()) > -1);
313         }
314     }
315 
316     /**
317      * @throws Exception if the test fails
318      */
319     @Test
320     @Alerts("gZip")
321     public void externalScriptGZipEncoded() throws Exception {
322         final MockWebConnection webConnection = getMockWebConnection();
323 
324         final String jsContent = "function doTest() { alert('gZip'); }";
325         final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
326         try (GZIPOutputStream gzipper = new GZIPOutputStream(bytes)) {
327             gzipper.write(jsContent.getBytes("ASCII"));
328         }
329 
330         final List<NameValuePair> headers = new ArrayList<>();
331         headers.add(new NameValuePair("Content-Encoding", "gzip"));
332         webConnection.setResponse(new URL(URL_FIRST, "foo.js"),
333                 bytes.toByteArray(), 200, "OK", "text/javascript", headers);
334 
335         final String htmlContent = DOCTYPE_HTML
336             + "<html><head>\n"
337             + "<title>foo</title>\n"
338             + "<script src='/foo.js'></script>\n"
339             + "</head><body onload='doTest()'>\n"
340             + "</body></html>";
341 
342         loadPageWithAlerts(htmlContent);
343     }
344 
345     /**
346      * Test for a javascript which points to an empty gzip encoded file (bug 3566999).
347      * @throws Exception if an error occurs
348      */
349     @Test
350     @Alerts("done")
351     public void externalScriptEmptyGZipEncoded() throws Exception {
352         final MockWebConnection webConnection = getMockWebConnection();
353 
354         try (ByteArrayOutputStream bytes = new ByteArrayOutputStream()) {
355             bytes.write("".getBytes("ASCII"));
356 
357             final List<NameValuePair> headers = new ArrayList<>();
358             headers.add(new NameValuePair(HttpHeader.CONTENT_LENGTH, "0"));
359             headers.add(new NameValuePair("Content-Encoding", "gzip"));
360             webConnection.setResponse(new URL(URL_FIRST, "foo.js"),
361                     bytes.toByteArray(), 200, "OK", "text/javascript", headers);
362         }
363 
364         final String htmlContent = DOCTYPE_HTML
365             + "<html><head>\n"
366             + "<title>foo</title>\n"
367             + "<script src='/foo.js'></script>\n"
368             + "</head><body onload='alert(\"done\");'>\n"
369             + "</body></html>";
370 
371         loadPageWithAlerts(htmlContent);
372     }
373 
374     /**
375      * Test for a javascript which points to a broken gzip encoded file (bug 3563712).
376      * @throws Exception if an error occurs
377      */
378     @Test
379     public void externalScriptBrokenGZipEncoded() throws Exception {
380         final MockWebConnection webConnection = getMockWebConnection();
381 
382         try (ByteArrayOutputStream bytes = new ByteArrayOutputStream()) {
383             final String jsContent = "function doTest() { alert('gZip'); }";
384             bytes.write(jsContent.getBytes("ASCII"));
385 
386             final List<NameValuePair> headers = new ArrayList<>();
387             headers.add(new NameValuePair("Content-Encoding", "gzip"));
388             webConnection.setResponse(new URL(URL_FIRST, "foo.js"),
389                     bytes.toByteArray(), 200, "OK", "text/javascript", headers);
390         }
391 
392         final String htmlContent = DOCTYPE_HTML
393             + "<html><head>\n"
394             + "<title>foo</title>\n"
395             + "<script src='/foo.js'></script>\n"
396             + "</head><body onload='doTest();alert(\"done\");'>\n"
397             + "</body></html>";
398 
399         try {
400             loadPageWithAlerts(htmlContent);
401             fail("ScriptException expected");
402         }
403         catch (final ScriptException e) {
404             // expected
405         }
406     }
407 
408     /**
409      * @throws Exception if the test fails
410      */
411     @Test
412     public void referencingVariablesFromOneScriptToAnother_Regression() throws Exception {
413         final WebClient client = getWebClient();
414         final MockWebConnection webConnection = new MockWebConnection();
415 
416         final String htmlContent = DOCTYPE_HTML
417             + "<html><head><title>foo</title><script src='./test.js'></script>\n"
418             + "<script>var testLocalVariable = new Array();</script>\n"
419             + "</head><body onload='testNestedMethod();' >\n"
420             + "<form name='form1' method='POST' action='../foo' >\n"
421             + "  <input type='submit' value='Login' name='loginButton'>\n"
422             + "</form></body></html>";
423 
424         final String jsContent
425             = "function testNestedMethod() {\n"
426             + "  if (testLocalVariable == null)\n"
427             + "    testLocalVariable = 'foo';\n"
428             + "} ";
429 
430         webConnection.setResponse(URL_FIRST, htmlContent);
431         webConnection.setResponse(new URL(URL_FIRST, "test.js"), jsContent, "text/javascript");
432         client.setWebConnection(webConnection);
433 
434         final HtmlPage page = client.getPage(URL_FIRST);
435         assertEquals("foo", page.getTitleText());
436     }
437 
438     /**
439      * @throws Exception if the test fails
440      */
441     @Test
442     public void javaScriptUrl() throws Exception {
443         final String htmlContent = DOCTYPE_HTML
444             + "<html><head><script language='javascript'>\n"
445             + "var f1 = '<html><head><title>frame1</title></head><body><h1>frame1</h1></body></html>';\n"
446             + "var f2 = '<html><head><title>frame2</title></head><body><h1>frame2</h1></body></html>';\n"
447             + "</script></head>\n"
448             + "<frameset border='0' frameborder='0' framespacing='0' rows='100,*'>\n"
449             + "  <frame id='frame1' src='javascript:parent.f1'/>\n"
450             + "  <frame id='frame2' src='javascript:parent.f2'/>\n"
451             + "</frameset></html>";
452 
453         final HtmlPage page = loadPage(htmlContent, null);
454 
455         final HtmlPage page1 = (HtmlPage) ((HtmlFrame) page.getHtmlElementById("frame1")).getEnclosedPage();
456         final HtmlPage page2 = (HtmlPage) ((HtmlFrame) page.getHtmlElementById("frame2")).getEnclosedPage();
457 
458         assertNotNull("page1", page1);
459         assertNotNull("page2", page2);
460 
461         assertEquals("frame1", page1.getTitleText());
462         assertEquals("frame2", page2.getTitleText());
463     }
464 
465     /**
466      * When using the syntax this.something in an onclick handler, "this" must represent
467      * the object being clicked, not the window. Regression test.
468      * @throws Exception if the test fails
469      */
470     @Test
471     public void thisDotInOnClick() throws Exception {
472         final String htmlContent = DOCTYPE_HTML
473              + "<html><head><title>First</title><script>function foo(message){alert(message);}</script><body>\n"
474              + "<form name='form1'><input type='submit' name='button1' onClick='foo(this.name)'></form>\n"
475              + "</body></html>";
476 
477         final List<String> collectedAlerts = new ArrayList<>();
478         final HtmlPage page = loadPage(htmlContent, collectedAlerts);
479         assertEquals("First", page.getTitleText());
480 
481         page.getFormByName("form1").getInputByName("button1").click();
482 
483         final String[] expectedAlerts = {"button1"};
484         assertEquals(expectedAlerts, collectedAlerts);
485     }
486 
487     /**
488      * @throws Exception if the test fails
489      */
490     @Test
491     public void functionDefinedInExternalFile_CalledFromInlineScript() throws Exception {
492         final WebClient client = getWebClient();
493         final MockWebConnection webConnection = new MockWebConnection();
494 
495         final String htmlContent = DOCTYPE_HTML
496             + "<html><head><title>foo</title><script src='./test.js'></script>\n"
497             + "</head><body>\n"
498             + "  <script>externalMethod()</script>\n"
499             + "</body></html>";
500 
501         final String jsContent
502             = "function externalMethod() {\n"
503             + "    alert('Got to external method');\n"
504             + "} ";
505 
506         webConnection.setResponse(
507             new URL("http://first/index.html"),
508             htmlContent);
509         webConnection.setResponse(
510             new URL("http://first/test.js"),
511             jsContent, "text/javascript");
512         client.setWebConnection(webConnection);
513 
514         final List<String> collectedAlerts = new ArrayList<>();
515         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
516 
517         final HtmlPage page = client.getPage("http://first/index.html");
518         assertEquals("foo", page.getTitleText());
519         assertEquals(new String[] {"Got to external method"}, collectedAlerts);
520     }
521 
522     /**
523      * Regression test for
524      * <a href="https://sourceforge.net/tracker/?func=detail&atid=448266&aid=1552746&group_id=47038">
525      * https://sourceforge.net/tracker/?func=detail&amp;atid=448266&amp;aid=1552746&amp;group_id=47038</a>.
526      * @throws Exception if the test fails
527      */
528     @Test
529     public void externalScriptWithNewLineBeforeClosingScriptTag() throws Exception {
530         final WebClient client = getWebClient();
531         final MockWebConnection webConnection = new MockWebConnection();
532 
533         final String htmlContent = DOCTYPE_HTML
534             + "<html><head><title>foo</title>\n"
535             + "</head><body>\n"
536             + "<script src='test.js'>\n</script>\n" // \n between opening and closing tag is important
537             + "</body></html>";
538 
539         final String jsContent
540             = "function externalMethod() {\n"
541             + "    alert('Got to external method');\n"
542             + "}\n"
543             + "externalMethod();\n";
544 
545         webConnection.setResponse(URL_FIRST, htmlContent);
546         webConnection.setDefaultResponse(jsContent, 200, "OK", "text/javascript");
547         client.setWebConnection(webConnection);
548 
549         final List<String> collectedAlerts = new ArrayList<>();
550         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
551 
552         client.getPage(URL_FIRST);
553         assertEquals(new String[] {"Got to external method"}, collectedAlerts);
554     }
555 
556     /**
557      * Test case for bug 707134. Currently I am unable to reproduce the problem.
558      * @throws Exception if the test fails
559      */
560     @Test
561     public void functionDefinedInSameFile() throws Exception {
562         final String htmlContent = DOCTYPE_HTML
563             + "<html><head><title>First</title><script>\n"
564             + "function showFoo(foo) {\n"
565             + "  alert('Foo is: |' + foo + '|');\n"
566             + "}\n"
567             + "</script>\n"
568             + "</head><body><form name='form1'>\n"
569             + "<input name='text1' type='text'>\n"
570             + "<input name='button1' type='button' onclick='showFoo(document.form1.text1.value);'>\n"
571             + "</form></body></html>";
572 
573         final List<String> collectedAlerts = new ArrayList<>();
574 
575         final HtmlPage page = loadPage(htmlContent, collectedAlerts);
576         assertEquals("First", page.getTitleText());
577 
578         final HtmlForm form = page.getFormByName("form1");
579         final HtmlTextInput textInput = form.getInputByName("text1");
580         textInput.setValue("flintstone");
581 
582         final HtmlButtonInput button = form.getInputByName("button1");
583         assertEquals(Collections.EMPTY_LIST, collectedAlerts);
584 
585         button.click();
586 
587         assertEquals(new String[] {"Foo is: |flintstone|"}, collectedAlerts);
588     }
589 
590     /**
591      * Test that the JavaScript engine gets called correctly for variable access.
592      * @throws Exception if the test fails
593      */
594     @Test
595     public void javaScriptEngineCallsForVariableAccess() throws Exception {
596         final WebClient client = getWebClient();
597         final MockWebConnection webConnection = new MockWebConnection();
598 
599         final List<String> collectedAlerts = new ArrayList<>();
600         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
601 
602         final String content = DOCTYPE_HTML
603             + "<html><head><title>foo</title><script>\n"
604             + "myDate = 'foo';\n"
605             + "function doUnqualifiedVariableAccess() {\n"
606             + "  alert('unqualified: ' + myDate);\n"
607             + "}\n"
608             + "function doQualifiedVariableAccess() {\n"
609             + "  alert('qualified: ' + window.myDate);\n"
610             + "}\n"
611             + "</script></head><body>\n"
612             + "<p>hello world</p>\n"
613             + "<a id='unqualified' onclick='doUnqualifiedVariableAccess();'>unqualified</a>\n"
614             + "<a id='qualified' onclick='doQualifiedVariableAccess();'>qualified</a>\n"
615             + "</body></html>";
616 
617         webConnection.setDefaultResponse(content);
618         client.setWebConnection(webConnection);
619         final CountingJavaScriptEngine countingJavaScriptEngine = new CountingJavaScriptEngine(client);
620         client.setJavaScriptEngine(countingJavaScriptEngine);
621 
622         final HtmlPage page = client.getPage(URL_FIRST);
623 
624         assertEquals(1, countingJavaScriptEngine.getExecutionCount());
625         assertEquals(0, countingJavaScriptEngine.getCallCount());
626 
627         page.getHtmlElementById("unqualified").click();
628         assertEquals(1, countingJavaScriptEngine.getExecutionCount());
629         assertEquals(1, countingJavaScriptEngine.getCallCount());
630 
631         page.getHtmlElementById("qualified").click();
632         assertEquals(1, countingJavaScriptEngine.getExecutionCount());
633         assertEquals(2, countingJavaScriptEngine.getCallCount());
634 
635         final String[] expectedAlerts = {"unqualified: foo", "qualified: foo"};
636         assertEquals(expectedAlerts, collectedAlerts);
637     }
638 
639     /**
640      * Tests ActiveX related exceptions that do not require a map.
641      * @throws Exception if the test fails
642      */
643     @Test
644     public void activeXObjectNoMap() throws Exception {
645         try {
646             loadPage(getJavaScriptContent("new ActiveXObject()"));
647             fail("An exception should be thrown for zero argument constructor.");
648         }
649         catch (final ScriptException e) {
650             // Success
651         }
652 
653         try {
654             loadPage(getJavaScriptContent("new ActiveXObject(1, '2', '3')"));
655             fail("An exception should be thrown for a three argument constructor.");
656         }
657         catch (final ScriptException e) {
658             // Success
659         }
660 
661         try {
662             loadPage(getJavaScriptContent("new ActiveXObject(a)"));
663             fail("An exception should be thrown for an undefined parameter in the constructor.");
664         }
665         catch (final ScriptException e) {
666             // Success
667         }
668 
669         try {
670             loadPage(getJavaScriptContent("new ActiveXObject(10)"));
671             fail("An exception should be thrown for an integer parameter in the constructor.");
672         }
673         catch (final ScriptException e) {
674             // Success
675         }
676 
677         try {
678             loadPage(getJavaScriptContent("new ActiveXObject('UnknownObject')"));
679             fail("An exception should be thrown for a null map.");
680         }
681         catch (final ScriptException e) {
682             // Success
683         }
684     }
685 
686     private static String getJavaScriptContent(final String javascript) {
687         return DOCTYPE_HTML
688              + "<html><head><title>foo</title><script>\n"
689              + javascript
690              + "</script></head><body>\n"
691              + "<p>hello world</p>\n"
692              + "<form name='form1'>\n"
693              + "  <input type='text' name='textfield1' id='textfield1' value='foo' />\n"
694              + "  <input type='text' name='textfield2' id='textfield2'/>\n"
695              + "</form>\n"
696              + "</body></html>";
697     }
698 
699     /**
700      * Check that wrong JavaScript just causes its context to fail but not the whole page.
701      * @throws Exception if something goes wrong
702      */
703     @Test
704     public void scriptErrorIsolated() throws Exception {
705         final String content = DOCTYPE_HTML
706             + "<html>\n"
707             + "<head>\n"
708             + "<script>alert(1);</script>\n"
709             + "<script>alert(2</script>\n"
710             + "<script>alert(3);</script>\n"
711             + "</head>\n"
712             + "<body onload='alert(4);notExisting()'>\n"
713             + "<button onclick='alert(5)'>click me</button>\n"
714             + "</body>\n"
715             + "</html>";
716 
717         final String[] expectedAlerts = {"1", "3", "4"};
718 
719         final WebClient client = getWebClient();
720         final MockWebConnection webConnection = new MockWebConnection();
721         webConnection.setDefaultResponse(content);
722         client.setWebConnection(webConnection);
723         final List<String> collectedAlerts = new ArrayList<>();
724         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
725 
726         // first test with script exceptions thrown
727         try {
728             client.getPage(URL_FIRST);
729             fail("Should have thrown a script error");
730         }
731         catch (final Exception e) {
732             // nothing
733         }
734         assertEquals(new String[] {"1"}, collectedAlerts);
735         collectedAlerts.clear();
736 
737         // and with script exception not thrown
738         client.getOptions().setThrowExceptionOnScriptError(false);
739         client.getPage(URL_FIRST);
740 
741         assertEquals(expectedAlerts, collectedAlerts);
742     }
743 
744     /**
745      * Test that prototype changes are made in the right scope.
746      * Problem reported by Bruce Faulnker in the dev mailing list.
747      * This is due to a Rhino bug:
748      * https://bugzilla.mozilla.org/show_bug.cgi?id=374918
749      * @throws Exception if something goes wrong
750      */
751     @Test
752     public void prototypeScope() throws Exception {
753         prototypeScope("String", "'some string'");
754         prototypeScope("Number", "9");
755         prototypeScope("Date", "new Date()");
756         prototypeScope("Function", "function() {}");
757         prototypeScope("Array", "[]");
758     }
759 
760     private void prototypeScope(final String name, final String value) throws Exception {
761         final String content1 = DOCTYPE_HTML
762             + "<html><head>\n"
763             + "<script>\n"
764             + "window.open('second.html', 'secondWindow');\n"
765             + "</script>\n"
766             + "</head><body></body></html>";
767 
768         final String content2 = DOCTYPE_HTML
769             + "<html><head>\n"
770             + "<script>\n"
771             + "alert('in page 2');\n"
772             + name + ".prototype.foo = function() {\n"
773             + "   alert('in foo');\n"
774             + "};\n"
775             + "var x = " + value + ";\n"
776             + "x.foo();\n"
777             + "</script>\n"
778             + "</head><body></body></html>";
779 
780         final String[] expectedAlerts = {"in page 2", "in foo"};
781 
782         final WebClient client = getWebClient();
783         final MockWebConnection webConnection = new MockWebConnection();
784         webConnection.setDefaultResponse(content2);
785         webConnection.setResponse(URL_FIRST, content1);
786         client.setWebConnection(webConnection);
787 
788         final List<String> collectedAlerts = new ArrayList<>();
789         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
790 
791         client.getPage(URL_FIRST);
792         assertEquals(expectedAlerts, collectedAlerts);
793     }
794 
795     /**
796      * @throws Exception if the test fails
797      */
798     @Test
799     public void timeout() throws Exception {
800         final WebClient client = getWebClient();
801         final long timeout = 2000;
802         final long oldTimeout = client.getJavaScriptTimeout();
803         client.setJavaScriptTimeout(timeout);
804 
805         try {
806             client.getOptions().setThrowExceptionOnScriptError(false);
807 
808             final String content = DOCTYPE_HTML + "<html><body><script>while(1) {}</script></body></html>";
809             final MockWebConnection webConnection = new MockWebConnection();
810             webConnection.setDefaultResponse(content);
811             client.setWebConnection(webConnection);
812 
813             final Exception[] exceptions = {null};
814             final Thread runner = new Thread() {
815                 @Override
816                 public void run() {
817                     try {
818                         client.getPage(URL_FIRST);
819                     }
820                     catch (final Exception e) {
821                         exceptions[0] = e;
822                     }
823                 }
824             };
825 
826             runner.start();
827 
828             runner.join(timeout * 2);
829             if (runner.isAlive()) {
830                 runner.interrupt();
831                 fail("Script was still running after timeout");
832             }
833             assertNull(exceptions[0]);
834         }
835         finally {
836             client.setJavaScriptTimeout(oldTimeout);
837         }
838     }
839 
840     private static final class CountingJavaScriptEngine extends JavaScriptEngine {
841         private int scriptExecutionCount_ = 0;
842         private int scriptCallCount_ = 0;
843         private int scriptCompileCount_ = 0;
844         private int scriptExecuteScriptCount_ = 0;
845 
846         /**
847          * Creates an instance.
848          * @param client the WebClient
849          */
850         protected CountingJavaScriptEngine(final WebClient client) {
851             super(client);
852         }
853 
854         /** {@inheritDoc} */
855         @Override
856         public Object execute(
857                 final HtmlPage page, final Scriptable scope,
858                 final String sourceCode, final String sourceName, final int startLine) {
859             scriptExecutionCount_++;
860             return super.execute(page, scope, sourceCode, sourceName, startLine);
861         }
862 
863         /** {@inheritDoc} */
864         @Override
865         public Object execute(final HtmlPage page, final Scriptable scope, final Script script) {
866             scriptExecuteScriptCount_++;
867             return super.execute(page, scope, script);
868         }
869 
870         /** {@inheritDoc} */
871         @Override
872         public Script compile(final HtmlPage page, final Scriptable scope,
873                 final String sourceCode, final String sourceName, final int startLine) {
874             scriptCompileCount_++;
875             return super.compile(page, scope, sourceCode, sourceName, startLine);
876         }
877 
878         /** {@inheritDoc} */
879         @Override
880         public Object callFunction(
881                 final HtmlPage page, final Function javaScriptFunction,
882                 final Scriptable thisObject, final Object[] args,
883                 final DomNode htmlElementScope) {
884             scriptCallCount_++;
885             return super.callFunction(page, javaScriptFunction, thisObject, args, htmlElementScope);
886         }
887 
888         /**
889          * @return the number of times that this engine has called functions
890          */
891         public int getCallCount() {
892             return scriptCallCount_;
893         }
894 
895         /**
896          * @return the number of times that this engine has executed code
897          */
898         public int getExecutionCount() {
899             return scriptExecutionCount_;
900         }
901 
902         /**
903          * @return the number of times that this engine has compiled code
904          */
905         public int getCompileCount() {
906             return scriptCompileCount_;
907         }
908 
909         /**
910          * @return the number of times that this engine has executed a compiled script
911          */
912         public int getExecuteScriptCount() {
913             return scriptExecuteScriptCount_;
914         }
915     }
916 
917     /**
918      * @throws Exception if the test fails
919      */
920     @Test
921     @Alerts({"", "ex thrown"})
922     public void commentNoDoubleSlash() throws Exception {
923         final String html = DOCTYPE_HTML
924             + "<html><head>\n"
925             + "<script><!-- alert(1);\n"
926             + " alert(2);\n"
927             + "alert(3) --></script>\n"
928             + "</head>\n"
929             + "<body>\n"
930             + "</body></html>";
931 
932         final String expectedExThrown = getExpectedAlerts()[1];
933         String exceptionThrown = "no ex";
934         try {
935             setExpectedAlerts(getExpectedAlerts()[0]);
936             loadPageWithAlerts(html);
937         }
938         catch (final ScriptException e) {
939             exceptionThrown = "ex thrown";
940             assertEquals(5, e.getFailingLineNumber());
941         }
942 
943         assertEquals(expectedExThrown, exceptionThrown);
944     }
945 
946     /**
947      * Test that compiled script are cached.
948      * @throws Exception if the test fails
949      */
950     @Test
951     public void compiledScriptCached() throws Exception {
952         final String content1 = DOCTYPE_HTML
953             + "<html><head><title>foo</title>\n"
954             + "<script src='script.js'></script>\n"
955             + "</head><body>\n"
956             + "<a href='page2.html'>to page 2</a>\n"
957             + "</body></html>";
958         final String content2 = DOCTYPE_HTML
959             + "<html><head><title>page 2</title>\n"
960             + "<script src='script.js'></script>\n"
961             + "</head><body>\n"
962             + "</body></html>";
963         final String script = "alert(document.title)";
964 
965         final WebClient client = getWebClient();
966         final MockWebConnection connection = new MockWebConnection();
967         client.setWebConnection(connection);
968         connection.setResponse(URL_FIRST, content1);
969         connection.setResponse(new URL(URL_FIRST, "page2.html"), content2);
970 
971         final List<NameValuePair> headersAllowingCache = new ArrayList<>();
972         headersAllowingCache.add(new NameValuePair("Last-Modified", "Sun, 15 Jul 2007 20:46:27 GMT"));
973         connection.setResponse(new URL(URL_FIRST, "script.js"), script,
974                 200, "ok", "text/javascript", headersAllowingCache);
975 
976         final CountingJavaScriptEngine countingJavaScriptEngine = new CountingJavaScriptEngine(client);
977         client.setJavaScriptEngine(countingJavaScriptEngine);
978 
979         final List<String> collectedAlerts = new ArrayList<>();
980         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
981         final HtmlPage page1 = client.getPage(URL_FIRST);
982         assertEquals(new String[] {"foo"}, collectedAlerts);
983         assertEquals(1, countingJavaScriptEngine.getExecuteScriptCount());
984         assertEquals(1, countingJavaScriptEngine.getCompileCount());
985 
986         collectedAlerts.clear();
987         page1.getAnchors().get(0).click();
988         assertEquals(new String[] {"page 2"}, collectedAlerts);
989         assertEquals(2, countingJavaScriptEngine.getExecuteScriptCount());
990         assertEquals(1, countingJavaScriptEngine.getCompileCount());
991     }
992 
993     /**
994      * Test that code in script tags is executed on page load. Try different combinations
995      * of the script tag except for the case where a remote JavaScript page is loaded. That
996      * one will be tested separately.
997      * @throws Exception if something goes wrong
998      */
999     @Test
1000     public void scriptTags_AllLocalContent() throws Exception {
1001         final String content = DOCTYPE_HTML
1002             + "<html>\n"
1003             + "<head><title>foo</title>\n"
1004             + "<script>One</script>\n" // no language specified - assume JavaScript
1005             + "<script language='javascript'>Two</script>\n"
1006             + "<script type='text/javascript'>Three</script>\n"
1007             + "<script type='text/perl'>Four</script>\n" // type is unsupported language
1008             + "</head>\n"
1009             + "<body>\n"
1010             + "<p>hello world</p>\n"
1011             + "</body></html>";
1012         final List<String> collectedScripts = new ArrayList<>();
1013         loadPageAndCollectScripts(content, collectedScripts);
1014 
1015         // NO MORE: The last expected is the dummy stub that is needed to initialize the JavaScript engine
1016         final String[] expectedScripts = {"One", "Two", "Three"};
1017 
1018         assertEquals(expectedScripts, collectedScripts);
1019     }
1020 
1021     private HtmlPage loadPageAndCollectScripts(final String html, final List<String> collectedScripts)
1022         throws Exception {
1023 
1024         final WebClient client = getWebClient();
1025         client.setJavaScriptEngine(new JavaScriptEngine(client) {
1026             @Override
1027             public Object execute(final HtmlPage htmlPage, final Scriptable scope,
1028                     final String sourceCode, final String sourceName, final int startLine) {
1029                 collectedScripts.add(sourceCode);
1030                 return null;
1031             }
1032             @Override
1033             public Object callFunction(
1034                     final HtmlPage htmlPage, final Function javaScriptFunction,
1035                     final Scriptable thisObject, final Object[] args,
1036                     final DomNode htmlElement) {
1037                 return null;
1038             }
1039             @Override
1040             public boolean isScriptRunning() {
1041                 return false;
1042             }
1043         });
1044 
1045         final MockWebConnection webConnection = new MockWebConnection();
1046 
1047         webConnection.setDefaultResponse(html);
1048         client.setWebConnection(webConnection);
1049 
1050         final HtmlPage page = client.getPage(new WebRequest(URL_FIRST, HttpMethod.POST));
1051         return page;
1052     }
1053 
1054     /**
1055      * Verifies that we're not using a global context factory, so that we can cleanly run multiple
1056      * WebClient instances concurrently within a single JVM. See bug #689.
1057      */
1058     @Test
1059     public void noGlobalContextFactoryUsed() {
1060         final WebClient client1 = getWebClient();
1061         final WebClient client2 = createNewWebClient();
1062 
1063         final HtmlUnitContextFactory cf1 = client1.getJavaScriptEngine().getContextFactory();
1064         final HtmlUnitContextFactory cf2 = client2.getJavaScriptEngine().getContextFactory();
1065 
1066         assertFalse(cf1 == cf2);
1067         assertFalse(cf1 == ContextFactory.getGlobal());
1068         assertFalse(cf2 == ContextFactory.getGlobal());
1069     }
1070 
1071     /**
1072      * Configure subclass of {@link JavaScriptEngine} that collects background JS expressions.
1073      * @throws Exception if the test fails
1074      */
1075     @Test
1076     public void catchBackgroundJSErrors() throws Exception {
1077         final WebClient webClient = getWebClient();
1078         final List<ScriptException> jsExceptions = new ArrayList<>();
1079         final JavaScriptEngine myEngine = new JavaScriptEngine(webClient) {
1080             @Override
1081             protected void handleJavaScriptException(final ScriptException scriptException,
1082                     final boolean triggerOnError) {
1083                 jsExceptions.add(scriptException);
1084                 super.handleJavaScriptException(scriptException, triggerOnError);
1085             }
1086         };
1087         webClient.setJavaScriptEngine(myEngine);
1088 
1089         final String html = DOCTYPE_HTML
1090             + "<html>\n"
1091             + "<head><title>Test page</title><\n"
1092             + "<script>\n"
1093             + "function myFunction() {\n"
1094             + "  document.title = 'New title';\n"
1095             + "  notExisting(); // will throw\n"
1096             + "}\n"
1097             + "window.setTimeout(myFunction, 5);\n"
1098             + "</script>\n"
1099             + "</head>\n"
1100             + "<body>\n"
1101             + "</body></html>";
1102 
1103         final MockWebConnection webConnection = new MockWebConnection();
1104         webConnection.setDefaultResponse(html);
1105         webClient.setWebConnection(webConnection);
1106 
1107         final HtmlPage page = webClient.getPage(URL_FIRST);
1108         webClient.waitForBackgroundJavaScript(10_000);
1109         assertEquals("New title", page.getTitleText());
1110 
1111         assertEquals(1, jsExceptions.size());
1112         final ScriptException exception = jsExceptions.get(0);
1113         assertTrue("Message: " + exception.getMessage(),
1114             exception.getMessage().contains("\"notExisting\" is not defined"));
1115     }
1116 
1117     /**
1118      * Ensures that the JS executor thread is a daemon thread.
1119      * @throws Exception if the test fails
1120      */
1121     @Test
1122     public void daemonExecutorThread() throws Exception {
1123         final String html = DOCTYPE_HTML
1124             + "<html><body><script>\n"
1125             + "function f() { alert('foo'); }\n"
1126             + "setTimeout(f, 5);\n"
1127             + "</script>\n"
1128             + "</body></html>";
1129 
1130         final List<String> collectedAlerts = new ArrayList<>();
1131         final HtmlPage page = loadPage(html, collectedAlerts);
1132 
1133         Thread.sleep(20);
1134         final List<Thread> jsThreads = getJavaScriptThreads();
1135         assertEquals(1, jsThreads.size());
1136         final Thread jsThread = jsThreads.get(0);
1137         assertEquals("JS executor for " + page.getWebClient(), jsThread.getName());
1138         assertTrue(jsThread.isDaemon());
1139     }
1140 
1141     /**
1142      * @throws Exception if the test fails
1143      */
1144     @Test
1145     public void shutdown() throws Exception {
1146         final String html = "<html></html>";
1147         final HtmlPage page = loadPage(html);
1148 
1149         @SuppressWarnings("resource")
1150         final WebClient webClient = getWebClient();
1151         final AbstractJavaScriptEngine<?> engine = webClient.getJavaScriptEngine();
1152 
1153         engine.addPostponedAction(new PostponedAction(page, "shutdown test") {
1154             @Override
1155             public void execute() throws Exception {
1156                 // empty
1157             }
1158         });
1159         assertEquals(1, getPostponedActions(engine).get().size());
1160         webClient.close();
1161 
1162         assertNull(getPostponedActions(engine).get());
1163     }
1164 
1165     @SuppressWarnings("unchecked")
1166     private static ThreadLocal<List<PostponedAction>> getPostponedActions(final AbstractJavaScriptEngine<?> engine) {
1167         try {
1168             final Field field = engine.getClass().getDeclaredField("postponedActions_");
1169             field.setAccessible(true);
1170             return (ThreadLocal<List<PostponedAction>>) field.get(engine);
1171         }
1172         catch (final Exception e) {
1173             throw Context.throwAsScriptRuntimeEx(e);
1174         }
1175     }
1176 
1177     /**
1178      * @throws Exception if the test fails
1179      */
1180     @Test
1181     @Retry
1182     @Alerts("starting")
1183     public void shutdownShouldKill() throws Exception {
1184         final String html = DOCTYPE_HTML
1185                 + "<html>\n"
1186                 + "<head><title>Test page</title>\n"
1187                 + "<script>\n"
1188                 + "  function test() {\n"
1189                 + "    alert('starting');\n"
1190                 + "    while(true) { Math.sin(3.14) * Math.sin(3.14);}\n"
1191                 + "  }\n"
1192                 + "</script>\n"
1193                 + "</head>\n"
1194                 + "<body onload='setTimeout(test, 50);'>\n"
1195                 + "</body></html>";
1196 
1197         try (WebClient webClient = getWebClient()) {
1198             // there is no way to kill a thread in later JDK's
1199             // to make the test running we need a final timeout for js stuff
1200             webClient.setJavaScriptTimeout(DEFAULT_WAIT_TIME.toMillis() * 10);
1201 
1202             final List<String> collectedAlerts = new ArrayList<>();
1203             webClient.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
1204 
1205             loadPage(html);
1206             Thread.sleep(100);
1207             assertEquals(getExpectedAlerts(), collectedAlerts);
1208 
1209         }
1210         Thread.sleep(400);
1211         try {
1212             assertTrue(getJavaScriptThreads().isEmpty());
1213         }
1214         catch (final AssertionError e) {
1215             Thread.sleep(DEFAULT_WAIT_TIME.toMillis() * 10);
1216             assertTrue(getJavaScriptThreads().isEmpty());
1217         }
1218     }
1219 
1220     /**
1221      * @throws Exception if the test fails
1222      */
1223     @Test
1224     @Retry
1225     @Alerts("starting")
1226     public void shutdownShouldKillJavaScriptTimeout() throws Exception {
1227         final String html = DOCTYPE_HTML
1228                 + "<html>\n"
1229                 + "<head><title>Test page</title>\n"
1230                 + "<script>\n"
1231                 + "  function test() {\n"
1232                 + "    alert('starting');\n"
1233                 + "    while(true) { Math.sin(3.14) * Math.sin(3.14);}\n"
1234                 + "  }\n"
1235                 + "</script>\n"
1236                 + "</head>\n"
1237                 + "<body onload='setTimeout(test, 50);'>\n"
1238                 + "</body></html>";
1239 
1240         try (WebClient webClient = getWebClient()) {
1241             webClient.setJavaScriptTimeout(DEFAULT_WAIT_TIME.toMillis());
1242 
1243             final List<String> collectedAlerts = new ArrayList<>();
1244             webClient.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
1245 
1246             loadPage(html);
1247             Thread.sleep(100);
1248             assertEquals(getExpectedAlerts(), collectedAlerts);
1249 
1250         }
1251         Thread.sleep(400);
1252         assertTrue(getJavaScriptThreads().isEmpty());
1253     }
1254 
1255     /**
1256      * @throws Exception if the test fails
1257      */
1258     @Test
1259     @Alerts("unload")
1260     public void shutdownOnUnload() throws Exception {
1261         final String html = DOCTYPE_HTML
1262                 + "<html>\n"
1263                 + "<head><title>Test page</title>\n"
1264                 + "<script>\n"
1265                 + "  window.onbeforeunload = function(e) {\n"
1266                 + "    window.open('about:blank');\n"
1267                 + "  }\n"
1268                 + "</script>\n"
1269                 + "</head>\n"
1270                 + "<body'>\n"
1271                 + "</body></html>";
1272 
1273         loadPage(html);
1274 
1275         getWebClient().close();
1276         assertTrue(getJavaScriptThreads().isEmpty());
1277     }
1278 
1279     /**
1280      * Test for issue #1658.
1281      * @throws Exception if the test fails
1282      */
1283     @Test
1284     public void nonStandardBrowserVersion() throws Exception {
1285         final BrowserVersion browser = new BrowserVersion.BrowserVersionBuilder(BrowserVersion.CHROME)
1286                 .setApplicationName("Explorer")
1287                 .setApplicationVersion("5.0")
1288                 .build();
1289 
1290         try (WebClient client = new WebClient(browser)) {
1291             client.openWindow(UrlUtils.URL_ABOUT_BLANK, "TestWindow");
1292         }
1293     }
1294 
1295     /**
1296      * Test case for issue #1668.
1297      *
1298      * @throws Exception if the test fails
1299      */
1300     @Test
1301     public void initRaceCondition() throws Exception {
1302         final String html = DOCTYPE_HTML
1303                 + "<html>\n"
1304                 + "<head><title>Test page</title><\n"
1305                 + "<script>\n"
1306                 + "  var d = document.visibilityState;\n"
1307                 + "</script>\n"
1308                 + "</head>\n"
1309                 + "<body>\n"
1310                 + "</body></html>";
1311 
1312         final MockWebConnection webConnection = new MockWebConnection();
1313         webConnection.setDefaultResponse(html);
1314         try (WebClient webClient = getWebClient()) {
1315             webClient.setWebConnection(webConnection);
1316 
1317             final WebWindow window1 = webClient.getCurrentWindow();
1318             final WebWindow window2 = webClient.openWindow(null, "window2", window1);
1319 
1320             final int runs = 100;
1321 
1322             final Thread t1 = new Thread(new Runnable() {
1323 
1324                 @Override
1325                 public void run() {
1326                     try {
1327                         for (int i = 0; i < runs; i++) {
1328                             webClient.getPage(window1, new WebRequest(URL_FIRST));
1329                         }
1330                     }
1331                     catch (final FailingHttpStatusCodeException | IOException e) {
1332                         throw new RuntimeException(e);
1333                     }
1334                 }
1335             });
1336 
1337             final Thread t2 = new Thread(new Runnable() {
1338 
1339                 @Override
1340                 public void run() {
1341                     try {
1342                         for (int i = 0; i < runs; i++) {
1343                             webClient.getPage(window2, new WebRequest(URL_FIRST));
1344                         }
1345                     }
1346                     catch (final FailingHttpStatusCodeException | IOException e) {
1347                         throw new RuntimeException(e);
1348                     }
1349                 }
1350             });
1351 
1352             t1.start();
1353             t2.start();
1354 
1355             t1.join();
1356             t2.join();
1357         }
1358     }
1359 
1360     /**
1361      * Test case where {@link JavaScriptEngine#registerWindowAndMaybeStartEventLoop(WebWindow)}
1362      * is being called after {@link JavaScriptEngine#shutdown()}.
1363      */
1364     @Test
1365     public void useAfterShutdownShouldNotCreateThreads() {
1366         @SuppressWarnings("resource")
1367         final WebClient webClient = getWebClient();
1368         final WebWindow window = webClient.getCurrentWindow();
1369         final AbstractJavaScriptEngine<?> engine = webClient.getJavaScriptEngine();
1370         webClient.close();
1371         engine.registerWindowAndMaybeStartEventLoop(window);
1372         assertTrue(getJavaScriptThreads().isEmpty());
1373     }
1374 }