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.host;
16  
17  import java.util.ArrayList;
18  import java.util.Collections;
19  import java.util.HashMap;
20  import java.util.List;
21  
22  import org.htmlunit.BrowserVersion;
23  import org.htmlunit.CollectingAlertHandler;
24  import org.htmlunit.MockWebConnection;
25  import org.htmlunit.SimpleWebTestCase;
26  import org.htmlunit.WebClient;
27  import org.htmlunit.corejs.javascript.BaseFunction;
28  import org.htmlunit.corejs.javascript.Context;
29  import org.htmlunit.corejs.javascript.Function;
30  import org.htmlunit.corejs.javascript.Scriptable;
31  import org.htmlunit.corejs.javascript.ScriptableObject;
32  import org.htmlunit.html.DomNode;
33  import org.htmlunit.html.HtmlDivision;
34  import org.htmlunit.html.HtmlElement;
35  import org.htmlunit.html.HtmlPage;
36  import org.junit.After;
37  import org.junit.Before;
38  import org.junit.Test;
39  
40  /**
41   * Tests for {@link Window} that use background jobs.
42   *
43   * @author Brad Clarke
44   * @author Daniel Gredler
45   * @author Frank Danek
46   * @author Ronald Brill
47   */
48  public class WindowConcurrencyTest extends SimpleWebTestCase {
49  
50      private WebClient client_;
51      private long startTime_;
52  
53      private void startTimedTest() {
54          startTime_ = System.currentTimeMillis();
55      }
56  
57      private void assertMaxTestRunTime(final long maxRunTimeMilliseconds) {
58          final long endTime = System.currentTimeMillis();
59          final long runTime = endTime - startTime_;
60          assertTrue("\nTest took too long to run and results may not be accurate. Please try again. "
61              + "\n  Actual Run Time: "
62              + runTime
63              + "\n  Max Run Time: "
64              + maxRunTimeMilliseconds, runTime < maxRunTimeMilliseconds);
65      }
66  
67      /**
68       * Sets up the tests.
69       */
70      @Override
71      @Before
72      public void before() {
73          client_ = new WebClient();
74      }
75  
76      /**
77       * Tears down the tests.
78       */
79      @After
80      public void after() {
81          client_.close();
82      }
83  
84      /**
85       * @throws Exception if the test fails
86       */
87      @Test
88      public void setTimeout() throws Exception {
89          final String content = DOCTYPE_HTML
90              + "<html><body><script language='JavaScript'>window.setTimeout('alert(\"Yo!\")',1);\n"
91              + "</script></body></html>";
92  
93          final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
94          loadPage(client_, content, collectedAlerts);
95          assertEquals(0, client_.waitForBackgroundJavaScript(1000));
96          assertEquals(new String[] {"Yo!"}, collectedAlerts);
97      }
98  
99      /**
100      * @throws Exception if the test fails
101      */
102     @Test
103     public void setTimeoutByReference() throws Exception {
104         final String content = DOCTYPE_HTML
105             + "<html><body><script language='JavaScript'>\n"
106             + "function doTimeout() {alert('Yo!');}\n"
107             + "window.setTimeout(doTimeout,1);\n"
108             + "</script></body></html>";
109 
110         final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
111         loadPage(client_, content, collectedAlerts);
112         assertEquals(0, client_.waitForBackgroundJavaScript(1000));
113         assertEquals(new String[] {"Yo!"}, collectedAlerts);
114     }
115 
116     /**
117      * Just tests that setting and clearing an interval doesn't throw an exception.
118      * @throws Exception if the test fails
119      */
120     @Test
121     public void setAndClearInterval() throws Exception {
122         final String content = DOCTYPE_HTML
123             + "<html><body>\n"
124             + "<script>\n"
125             + "window.setInterval('alert(\"Yo!\")', 500);\n"
126             + "function foo() { alert('Yo2'); }\n"
127             + "var i = window.setInterval(foo, 500);\n"
128             + "window.clearInterval(i);\n"
129             + "</script></body></html>";
130 
131         final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
132         loadPage(client_, content, collectedAlerts);
133     }
134 
135     /**
136      * @throws Exception if the test fails
137      */
138     @Test
139     public void setIntervalFunctionReference() throws Exception {
140         final String content = DOCTYPE_HTML
141             + "<html>\n"
142             + "<head>\n"
143             + "  <title>test</title>\n"
144             + "  <script>\n"
145             + "    var threadID;\n"
146             + "    function test() {\n"
147             + "      threadID = setInterval(doAlert, 100);\n"
148             + "    }\n"
149             + "    var iterationNumber = 0;\n"
150             + "    function doAlert() {\n"
151             + "      alert('blah');\n"
152             + "      if (++iterationNumber >= 3) {\n"
153             + "        clearInterval(threadID);\n"
154             + "      }\n"
155             + "    }\n"
156             + "  </script>\n"
157             + "</head>\n"
158             + "<body onload='test()'>\n"
159             + "</body>\n"
160             + "</html>";
161 
162         final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
163         loadPage(client_, content, collectedAlerts);
164         assertEquals(0, client_.waitForBackgroundJavaScript(1000));
165         assertEquals(Collections.nCopies(3, "blah"), collectedAlerts);
166     }
167 
168     /**
169      * @throws Exception if the test fails
170      */
171     @Test
172     public void clearInterval() throws Exception {
173         final String html = DOCTYPE_HTML
174             + "<html><body onload='test()'><script>\n"
175             + "  var count;\n"
176             + "  var id;\n"
177             + "  function test() {\n"
178             + "    count = 0;\n"
179             + "    id = setInterval(callback, 100);\n"
180             + "  }\n"
181             + "  function callback() {\n"
182             + "    count++;\n"
183             + "    clearInterval(id);\n"
184             + "    // Give the callback time to show its ugly face.\n"
185             + "    // If it fires between now and then, we'll know.\n"
186             + "    setTimeout('alert(count)', 500);\n"
187             + "  }\n"
188             + "</script></body></html>";
189         final String[] expected = {"1"};
190         final List<String> actual = Collections.synchronizedList(new ArrayList<String>());
191         startTimedTest();
192         loadPage(client_, html, actual);
193         assertEquals(0, client_.waitForBackgroundJavaScript(10_000));
194         assertEquals(expected, actual);
195         assertMaxTestRunTime(5000);
196     }
197 
198     /**
199      * Test that a script started by a timer is stopped if the page that started it
200      * is not loaded anymore.
201      * @throws Exception if the test fails
202      */
203     @Test
204     public void setTimeoutStopped() throws Exception {
205         final String firstContent = DOCTYPE_HTML
206             + "<html><head>\n"
207             + "<script language='JavaScript'>window.setTimeout('alert(\"Yo!\")', 10000);</script>\n"
208             + "</head><body onload='document.location.replace(\"" + URL_SECOND + "\")'></body></html>";
209         final String secondContent = DOCTYPE_HTML
210                 + "<html><head><title>Second</title></head><body></body></html>";
211 
212         final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
213         client_.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
214 
215         final MockWebConnection webConnection = new MockWebConnection();
216         webConnection.setResponse(URL_FIRST, firstContent);
217         webConnection.setResponse(URL_SECOND, secondContent);
218         client_.setWebConnection(webConnection);
219 
220         final HtmlPage page = client_.getPage(URL_FIRST);
221         assertEquals(0, page.getWebClient().waitForBackgroundJavaScript(2000));
222         assertEquals("Second", page.getTitleText());
223         assertEquals(Collections.EMPTY_LIST, collectedAlerts);
224     }
225 
226     /**
227      * @throws Exception if the test fails
228      */
229     @Test
230     public void clearTimeout() throws Exception {
231         final String content = DOCTYPE_HTML
232             + "<html>\n"
233             + "<head>\n"
234             + "  <title>test</title>\n"
235             + "  <script>\n"
236             + "    function test() {\n"
237             + "      var id = setTimeout('doAlert()', 2000);\n"
238             + "      clearTimeout(id);\n"
239             + "    }\n"
240             + "    function doAlert() {\n"
241             + "      alert('blah');\n"
242             + "    }\n"
243             + "  </script>\n"
244             + "</head>\n"
245             + "<body onload='test()'>\n"
246             + "</body>\n"
247             + "</html>";
248         final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
249         loadPage(client_, content, collectedAlerts);
250         client_.waitForBackgroundJavaScript(2000);
251         assertEquals(Collections.EMPTY_LIST, collectedAlerts);
252     }
253 
254     /**
255      * Verifies that calling clearTimeout() on a callback which has already fired
256      * does not affect said callback.
257      * @throws Exception if the test fails
258      */
259     @Test
260     public void clearTimeout_DoesNotStopExecutingCallback() throws Exception {
261         final String html = DOCTYPE_HTML
262             + "<html><body onload='test()'><script>\n"
263             + "  var id;\n"
264             + "  function test() {\n"
265             + "    id = setTimeout(callback, 1);\n"
266             + "  }\n"
267             + "  function callback() {\n"
268             + "    alert(id != 0);\n"
269             + "    clearTimeout(id);\n"
270             + "    // Make sure we weren't stopped.\n"
271             + "    alert('completed');\n"
272             + "  }\n"
273             + "</script><div id='a'></div></body></html>";
274         final String[] expected = {"true", "completed"};
275         final List<String> actual = Collections.synchronizedList(new ArrayList<String>());
276         loadPage(client_, html, actual);
277         client_.waitForBackgroundJavaScript(5000);
278         assertEquals(expected, actual);
279     }
280 
281     /**
282      * Tests that nested setTimeouts that are deeper than Thread.MAX_PRIORITY
283      * do not cause an exception.
284      * @throws Exception if the test fails
285      */
286     @Test
287     public void nestedSetTimeoutAboveMaxPriority() throws Exception {
288         final int max = Thread.MAX_PRIORITY + 1;
289         final String content = DOCTYPE_HTML
290             + "<html><body><script language='JavaScript'>\n"
291             + "var depth = 0;\n"
292             + "var maxdepth = " + max + ";\n"
293             + "function addAnother() {\n"
294             + "  if (depth < maxdepth) {\n"
295             + "    window.alert('ping');\n"
296             + "    depth++;\n"
297             + "    window.setTimeout('addAnother();', 1);\n"
298             + "  }\n"
299             + "}\n"
300             + "addAnother();\n"
301             + "</script></body></html>";
302 
303         final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
304         loadPage(client_, content, collectedAlerts);
305         assertEquals(0, client_.waitForBackgroundJavaScript((max + 1) * 1000));
306         assertEquals(Collections.nCopies(max, "ping"), collectedAlerts);
307     }
308 
309     /**
310      * Regression test for bug #693 with clearInterval.
311      * @see <a href="http://sourceforge.net/p/htmlunit/bugs/693/">bug details</a>
312      * @throws Exception if the test fails
313      */
314     @Test
315     public void clearInterval_threadInterrupt() throws Exception {
316         doTestClearX_threadInterrupt("Interval");
317     }
318 
319     /**
320      * Regression test for bug #2093370 with clearTimeout.
321      * @see <a href="http://sourceforge.net/p/htmlunit/bugs/693/">bug details</a>
322      * @throws Exception if the test fails
323      */
324     @Test
325     public void clearTimeout_threadInterrupt() throws Exception {
326         doTestClearX_threadInterrupt("Timeout");
327     }
328 
329     private void doTestClearX_threadInterrupt(final String x) throws Exception {
330         final String html = DOCTYPE_HTML
331             + "<html><head><title>foo</title><script>\n"
332             + "  function f() {\n"
333             + "    alert('started');\n"
334             + "    clear" + x + "(window.timeoutId);\n"
335             + "    mySpecialFunction();\n"
336             + "    alert('finished');\n"
337             + "  }\n"
338             + "  function test() {\n"
339             + "    window.timeoutId = set" + x + "(f, 10);\n"
340             + "  }\n"
341             + "</script></head><body>\n"
342             + "<span id='clickMe' onclick='test()'>click me</span>\n"
343             + "</body></html>";
344 
345         final String[] expectedAlerts = {"started", "finished"};
346 
347         final List<String> collectedAlerts = new ArrayList<>();
348         final HtmlPage page = loadPage(client_, html, collectedAlerts);
349         final Function mySpecialFunction = new BaseFunction() {
350             @Override
351             public Object call(final Context cx, final Scriptable scope,
352                     final Scriptable thisObj, final Object[] args) {
353                 if (Thread.currentThread().isInterrupted()) {
354                     throw new RuntimeException("My thread is already interrupted");
355                 }
356                 return null;
357             }
358         };
359         final ScriptableObject window = page.getEnclosingWindow().getScriptableObject();
360         ScriptableObject.putProperty(window, "mySpecialFunction", mySpecialFunction);
361         page.getHtmlElementById("clickMe").click();
362         client_.waitForBackgroundJavaScript(5000);
363         assertEquals(expectedAlerts, collectedAlerts);
364     }
365 
366     /**
367      * Verifies that when all windows are closed, background JS jobs are stopped (see bug 2127419).
368      * @throws Exception if the test fails
369      */
370     @Test
371     public void verifyCloseStopsJavaScript() throws Exception {
372         final String html = DOCTYPE_HTML
373             + "<html><head><title>foo</title><script>\n"
374             + "  function f() {\n"
375             + "    alert('Oh no!');\n"
376             + "  }\n"
377             + "  function test() {\n"
378             + "    window.timeoutId = setInterval(f, 1000);\n"
379             + "  }\n"
380             + "</script></head><body onload='test()'>\n"
381             + "</body></html>";
382 
383         final List<String> collectedAlerts = new ArrayList<>();
384         client_.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
385 
386         final MockWebConnection webConnection = new MockWebConnection();
387         webConnection.setDefaultResponse(html);
388         client_.setWebConnection(webConnection);
389 
390         client_.getPage(URL_FIRST);
391         client_.close();
392         client_.waitForBackgroundJavaScript(5000);
393         assertTrue(collectedAlerts.isEmpty());
394     }
395 
396     /**
397      * Verifies that when you go to a new page, background JS jobs are stopped (see bug 2127419).
398      * @throws Exception if the test fails
399      */
400     @Test
401     public void verifyGoingToNewPageStopsJavaScript() throws Exception {
402         final String html1 = DOCTYPE_HTML
403             + "<html><head><title>foo</title><script>\n"
404             + "  function f() {\n"
405             + "    alert('Oh no!');\n"
406             + "  }\n"
407             + "  function test() {\n"
408             + "    window.timeoutId = setInterval(f, 1000);\n"
409             + "  }\n"
410             + "</script></head><body onload='test()'>\n"
411             + "</body></html>";
412         final String html2 = "<html></html>";
413 
414         final List<String> collectedAlerts = new ArrayList<>();
415         client_.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
416 
417         final MockWebConnection conn = new MockWebConnection();
418         conn.setResponse(URL_FIRST, html1);
419         conn.setResponse(URL_SECOND, html2);
420         client_.setWebConnection(conn);
421 
422         client_.getPage(URL_FIRST);
423         client_.getPage(URL_SECOND);
424 
425         client_.waitForBackgroundJavaScript(5000);
426         client_.waitForBackgroundJavaScript(5000);
427 
428         assertTrue(collectedAlerts.isEmpty());
429     }
430 
431     /**
432      * Our Window proxy caused troubles.
433      * @throws Exception if the test fails
434      */
435     @Test
436     public void setTimeoutOnFrameWindow() throws Exception {
437         final String html = DOCTYPE_HTML
438             + "<html><head><title>foo</title><script>\n"
439             + "  function test() {\n"
440             + "    frames[0].setTimeout(f, 0);\n"
441             + "  }\n"
442             + "  function f() {\n"
443             + "    alert('in f');\n"
444             + "  }\n"
445             + "</script></head><body onload='test()'>\n"
446             + "<iframe src='about:blank'></iframe>\n"
447             + "</body></html>";
448 
449         final List<String> collectedAlerts = new ArrayList<>();
450         client_.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
451 
452         final MockWebConnection conn = new MockWebConnection();
453         conn.setDefaultResponse(html);
454         client_.setWebConnection(conn);
455 
456         client_.getPage(URL_FIRST);
457         assertEquals(0, client_.waitForBackgroundJavaScriptStartingBefore(1000));
458 
459         final String[] expectedAlerts = {"in f"};
460         assertEquals(expectedAlerts, collectedAlerts);
461     }
462 
463     /**
464      * Regression test for
465      * <a href="http://sourceforge.net/support/tracker.php?aid=2820051">bug 2820051</a>.
466      * @throws Exception if the test fails
467      */
468     @Test
469     public void concurrentModificationException_computedStyles() throws Exception {
470         final String html = DOCTYPE_HTML
471             + "<html><head><script>\n"
472             + "function test() {\n"
473             + "  getComputedStyle(document.body, null);\n"
474             + "}\n"
475             + "</script></head><body onload='test()'>\n"
476             + "<iframe src='foo.html' name='myFrame' id='myFrame'></iframe>\n"
477             + "</body></html>";
478 
479         final String html2 = DOCTYPE_HTML
480             + "<html><head><script>\n"
481             + "function forceStyleComputationInParent() {\n"
482             + "  var newNode = parent.document.createElement('span');\n"
483             + "  parent.document.body.appendChild(newNode);\n"
484             + "  parent.getComputedStyle(newNode, null);\n"
485             + "}\n"
486             + "setInterval(forceStyleComputationInParent, 10);\n"
487             + "</script></head></body></html>";
488 
489         try (WebClient client = new WebClient(BrowserVersion.FIREFOX)) {
490             final MockWebConnection webConnection = new MockWebConnection();
491             webConnection.setResponse(URL_FIRST, html);
492             webConnection.setDefaultResponse(html2);
493             client.setWebConnection(webConnection);
494 
495             final HtmlPage page1 = client.getPage(URL_FIRST);
496 
497             // Recreating what can occur with two threads requires
498             // to know a bit about the style invalidation used in Window.DomHtmlAttributeChangeListenerImpl
499             final HtmlElement elt = new HtmlDivision("div", page1, new HashMap<>()) {
500                 @Override
501                 public DomNode getParentNode() {
502                     // this gets called by CSS invalidation logic
503                     try {
504                         Thread.sleep(1000); // enough to let setInterval run
505                     }
506                     catch (final InterruptedException e) {
507                         throw new RuntimeException(e);
508                     }
509                     return super.getParentNode();
510                 }
511             };
512             page1.getBody().appendChild(elt);
513         }
514     }
515 }