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