View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import static org.htmlunit.WebDriverTestCase.LOG_TITLE_FUNCTION;
18  import static org.htmlunit.WebDriverTestCase.LOG_WINDOW_NAME_FUNCTION;
19  
20  import java.io.IOException;
21  import java.net.URL;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.List;
25  
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.htmlunit.html.HtmlPage;
29  import org.htmlunit.javascript.background.JavaScriptJobManager;
30  import org.junit.jupiter.api.Test;
31  import org.junitpioneer.jupiter.RetryingTest;
32  
33  /**
34   * Tests for {@link WebClient#waitForBackgroundJavaScriptStartingBefore(long)} and
35   * {@link WebClient#waitForBackgroundJavaScript(long)}.
36   *
37   * @author Marc Guillemot
38   * @author Ronald Brill
39   */
40  public class WebClientWaitForBackgroundJobsTest extends SimpleWebTestCase {
41  
42      private long startTime_;
43  
44      private void startTimedTest() {
45          startTime_ = System.currentTimeMillis();
46      }
47  
48      private void assertMaxTestRunTime(final long maxRunTimeMilliseconds) {
49          final long endTime = System.currentTimeMillis();
50          final long runTime = endTime - startTime_;
51          assertTrue("\nTest took too long to run and results may not be accurate. Please try again. "
52              + "\n  Actual Run Time: "
53              + runTime
54              + "\n  Max Run Time: "
55              + maxRunTimeMilliseconds, runTime < maxRunTimeMilliseconds);
56      }
57  
58      /**
59       * @throws Exception if the test fails
60       */
61      @Test
62      public void dontWaitWhenUnnecessary() throws Exception {
63          final String content = DOCTYPE_HTML
64              + "<html>\n"
65              + "<head>\n"
66              + "  <title>test</title>\n"
67              + "  <script>\n"
68              + LOG_TITLE_FUNCTION
69              + "    var threadID;\n"
70              + "    function test() {\n"
71              + "      threadID = setTimeout(doLog, 10000);\n"
72              + "    }\n"
73              + "    function doLog() {\n"
74              + "      log('blah');\n"
75              + "    }\n"
76              + "  </script>\n"
77              + "</head>\n"
78              + "<body onload='test()'>\n"
79              + "</body>\n"
80              + "</html>";
81  
82          final HtmlPage page = loadPage(content);
83          assertEquals("test", page.getTitleText());
84          final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager();
85          assertNotNull(jobManager);
86          assertEquals(1, jobManager.getJobCount());
87  
88          startTimedTest();
89          assertEquals(1, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(7000));
90          assertMaxTestRunTime(100);
91          assertEquals(1, jobManager.getJobCount());
92          assertEquals("test", page.getTitleText());
93      }
94  
95      /**
96       * @throws Exception if the test fails
97       */
98      @RetryingTest(3)
99      public void dontWaitWhenUnnecessary_jobRemovesOtherJob() throws Exception {
100         final String content = DOCTYPE_HTML
101             + "<html>\n"
102             + "<head>\n"
103             + "  <title>test</title>\n"
104             + "  <script>\n"
105             + LOG_TITLE_FUNCTION
106             + "    var longTimeoutID;\n"
107             + "    function test() {\n"
108             + "      longTimeoutID = setTimeout(doLog('late'), 10000);\n"
109             + "      setTimeout(clearLongTimeout, 100);\n"
110             + "      setTimeout(doLog('hello'), 300);\n"
111             + "    }\n"
112             + "    function clearLongTimeout() {\n"
113             + "      log('clearLongTimeout');\n"
114             + "      clearTimeout(longTimeoutID);\n"
115             + "    }\n"
116             + "    function doLog(_text) {\n"
117             + "      return function doLog() {\n"
118             + "        log(_text);\n"
119             + "      }\n"
120             + "    }\n"
121             + "  </script>\n"
122             + "</head>\n"
123             + "<body onload='test()'>\n"
124             + "</body>\n"
125             + "</html>";
126 
127         final HtmlPage page = loadPage(content);
128         assertEquals("test", page.getTitleText());
129         final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager();
130         assertNotNull(jobManager);
131         assertEquals(3, jobManager.getJobCount());
132 
133         startTimedTest();
134         assertEquals(0, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(20_000));
135         assertMaxTestRunTime(400);
136         assertEquals(0, jobManager.getJobCount());
137 
138         assertEquals("testclearLongTimeout§hello§", page.getTitleText());
139     }
140 
141     /**
142      * When waitForBackgroundJavaScriptStartingBefore is called while a job is being executed, it has
143      * to wait for this job to finish, even if this clearXXX has been called for it.
144      * @throws Exception if the test fails
145      */
146     @RetryingTest(3)
147     public void waitCalledDuringJobExecution() throws Exception {
148         final String html = DOCTYPE_HTML
149             + "<html>\n"
150             + "<head>\n"
151             + "  <title>test</title>\n"
152             + "  <script>\n"
153             + LOG_TITLE_FUNCTION
154             + "    var intervalId;\n"
155             + "    function test() {\n"
156             + "      intervalId = setTimeout(doWork, 100);\n"
157             + "    }\n"
158             + "    function doWork() {\n"
159             + "      clearTimeout(intervalId);\n"
160             + "      // waitForBackgroundJavaScriptStartingBefore should be called when JS execution is here\n"
161             + "      var request = new XMLHttpRequest();\n"
162             + "      request.open('GET', 'wait', false);\n"
163             + "      request.send('');\n"
164             + "      log('end work');\n"
165             + "    }\n"
166             + "  </script>\n"
167             + "</head>\n"
168             + "<body onload='test()'>\n"
169             + "</body>\n"
170             + "</html>";
171 
172         final ThreadSynchronizer threadSynchronizer = new ThreadSynchronizer();
173         final MockWebConnection webConnection = new MockWebConnection() {
174             @Override
175             public WebResponse getResponse(final WebRequest request) throws IOException {
176                 if (request.getUrl().toExternalForm().endsWith("/wait")) {
177                     threadSynchronizer.waitForState("just before waitForBackgroundJavaScriptStartingBefore");
178                     threadSynchronizer.sleep(400); // main thread need to be able to process next instruction
179                 }
180                 return super.getResponse(request);
181             }
182         };
183         webConnection.setResponse(URL_FIRST, html);
184         webConnection.setDefaultResponse("");
185 
186         final WebClient client = getWebClient();
187         client.setWebConnection(webConnection);
188 
189         final HtmlPage page = client.getPage(URL_FIRST);
190         assertEquals("test", page.getTitleText());
191         final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager();
192         assertNotNull(jobManager);
193         assertEquals(1, jobManager.getJobCount());
194 
195         startTimedTest();
196         threadSynchronizer.setState("just before waitForBackgroundJavaScriptStartingBefore");
197         assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(20_000));
198         assertMaxTestRunTime(600);
199         assertEquals(0, jobManager.getJobCount());
200 
201         assertEquals("testend work§", page.getTitleText());
202     }
203 
204     /**
205      * When waitForBackgroundJavaScriptStartingBefore is called and a new job is scheduled after the one that
206      * is first found as the last one within the delay (a job starts a new one or simply a setInterval),
207      * a few retries should be done to see if new jobs exists.
208      * @throws Exception if the test fails
209      */
210     @Test
211     public void waitWhenLastJobStartsNewOne() throws Exception {
212         final String html = DOCTYPE_HTML
213             + "<html>\n"
214             + "<head>\n"
215             + "  <title>test</title>\n"
216             + "  <script>\n"
217             + LOG_TITLE_FUNCTION
218             + "    function test() {\n"
219             + "      setTimeout(doWork1, 200);\n"
220             + "    }\n"
221             + "    function doWork1() {\n"
222             + "      log('work1');\n"
223             + "      setTimeout(doWork2, 200);\n"
224             + "    }\n"
225             + "    function doWork2() {\n"
226             + "      log('work2');\n"
227             + "    }\n"
228             + "  </script>\n"
229             + "</head>\n"
230             + "<body onload='test()'>\n"
231             + "</body>\n"
232             + "</html>";
233 
234         final HtmlPage page = loadPage(html);
235         assertEquals("test", page.getTitleText());
236         final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager();
237         assertNotNull(jobManager);
238         assertEquals(1, jobManager.getJobCount());
239 
240         startTimedTest();
241         assertEquals(0, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(20_000));
242         assertMaxTestRunTime(1000);
243         assertEquals(0, jobManager.getJobCount());
244 
245         assertEquals("testwork1§work2§", page.getTitleText());
246     }
247 
248     /**
249      * When waitForBackgroundJavaScriptStartingBefore is called and a new job is scheduled after the one that
250      * is first found as the last one within the delay (a job starts a new one or simply a setInterval),
251      * a few retries should be done to see if new jobs exists.
252      * @throws Exception if the test fails
253      */
254     @RetryingTest(3)
255     public void waitWithsubWindows() throws Exception {
256         final String html = DOCTYPE_HTML
257             + "<html>\n"
258             + "<head>\n"
259             + "  <title>test</title>\n"
260             + "</head>\n"
261             + "<body>\n"
262             + "<iframe src='nested.html'></iframe>\n"
263             + "</body>\n"
264             + "</html>";
265         final String nested = DOCTYPE_HTML
266             + "<html>\n"
267             + "<head>\n"
268             + "  <title>nested</title>\n"
269             + "  <script>\n"
270             + LOG_WINDOW_NAME_FUNCTION
271             + "    function test() {\n"
272             + "      setTimeout(doWork1, 200);\n"
273             + "    }\n"
274             + "    function doWork1() {\n"
275             + "      log('work1');\n"
276             + "    }\n"
277             + "  </script>\n"
278             + "</head>\n"
279             + "<body onload='test()'>\n"
280             + "</body>\n"
281             + "</html>";
282 
283         final WebClient client = getWebClient();
284 
285         final MockWebConnection webConnection = new MockWebConnection();
286         webConnection.setResponse(URL_FIRST, html);
287         webConnection.setDefaultResponse(nested);
288 
289         client.setWebConnection(webConnection);
290 
291         final HtmlPage page = client.getPage(URL_FIRST);
292         assertEquals("", page.getEnclosingWindow().getName());
293 
294         final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager();
295         assertNotNull(jobManager);
296         assertEquals(0, jobManager.getJobCount());
297 
298         startTimedTest();
299         assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(20_000));
300         assertMaxTestRunTime(300);
301         assertEquals(0, jobManager.getJobCount());
302 
303         assertEquals("work1§", page.getEnclosingWindow().getName());
304     }
305 
306     /**
307      * Test the case where a job is being executed at the time where waitForBackgroundJavaScriptStartingBefore
308      * and where this jobs schedules a new job after call to waitForBackgroundJavaScriptStartingBefore,
309      * .
310      * @throws Exception if the test fails
311      */
312     @RetryingTest(3)
313     public void newJobStartedAfterWait() throws Exception {
314         final String html = DOCTYPE_HTML
315             + "<html>\n"
316             + "<head>\n"
317             + "  <title>test</title>\n"
318             + "  <script>\n"
319             + LOG_TITLE_FUNCTION
320             + "    var request;\n"
321             + "    function onReadyStateChange() {\n"
322             + "      if (request.readyState == 4) {\n"
323             + "        log('xhr onchange');\n"
324             + "        setTimeout(doWork1, 200);\n"
325             + "      }\n"
326             + "    }\n"
327             + "    function test() {\n"
328             + "      request = new XMLHttpRequest();\n"
329             + "      request.open('GET', 'wait', true);\n"
330             + "      request.onreadystatechange = onReadyStateChange;\n"
331             + "      // waitForBackgroundJavaScriptStartingBefore should be called when JS execution is in send()\n"
332             + "      request.send('');\n"
333             + "    }\n"
334             + "    function doWork1() {\n"
335             + "      log('work1');\n"
336             + "    }\n"
337             + "  </script>\n"
338             + "</head>\n"
339             + "<body onload='test()'>\n"
340             + "</body>\n"
341             + "</html>";
342 
343         final ThreadSynchronizer threadSynchronizer = new ThreadSynchronizer();
344         final MockWebConnection webConnection = new MockWebConnection() {
345             @Override
346             public WebResponse getResponse(final WebRequest request) throws IOException {
347                 if (request.getUrl().toExternalForm().endsWith("/wait")) {
348                     threadSynchronizer.waitForState("just before waitForBackgroundJavaScriptStartingBefore");
349                     threadSynchronizer.sleep(400); // main thread need to be able to process next instruction
350                 }
351                 return super.getResponse(request);
352             }
353         };
354         webConnection.setResponse(URL_FIRST, html);
355         webConnection.setDefaultResponse("");
356 
357         final WebClient client = getWebClient();
358         client.setWebConnection(webConnection);
359 
360         final HtmlPage page = client.getPage(URL_FIRST);
361         assertEquals("test", page.getTitleText());
362         final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager();
363         assertNotNull(jobManager);
364         assertEquals(1, jobManager.getJobCount());
365 
366         startTimedTest();
367         threadSynchronizer.setState("just before waitForBackgroundJavaScriptStartingBefore");
368         assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(20_000));
369         assertMaxTestRunTime(1000);
370         assertEquals(0, jobManager.getJobCount());
371 
372         assertEquals("testxhr onchange§work1§", page.getTitleText());
373     }
374 
375     /**
376      * Tests that waitForBackgroundJavaScriptStartingBefore waits for jobs that should have been started earlier
377      * but that are "late" due to processing of previous job.
378      * This test needs to start many setTimeout to expect to reach the state, where a check for future
379      * jobs occurs when one of this job is not active.
380      * @throws Exception if the test fails
381      */
382     @RetryingTest(3)
383     public void waitForJobThatIsAlreadyLate() throws Exception {
384         final String html = DOCTYPE_HTML
385             + "<html>\n"
386             + "<head>\n"
387             + "  <script>\n"
388             + LOG_TITLE_FUNCTION
389             + "    var counter = 0;\n"
390             + "    function test() {\n"
391             + "      setTimeout(doWork1, 0);\n"
392             + "    }\n"
393             + "    function doWork1() {\n"
394             + "      if (counter++ < 50) {\n"
395             + "        setTimeout(doWork1, 0);\n"
396             + "      }\n"
397             + "      log('work' + counter);\n"
398             + "    }\n"
399             + "  </script>\n"
400             + "</head>\n"
401             + "<body onload='test()'>\n"
402             + "</body>\n"
403             + "</html>";
404 
405         final MockWebConnection webConnection = new MockWebConnection();
406         webConnection.setResponse(URL_FIRST, html);
407         webConnection.setDefaultResponse("");
408 
409         final WebClient client = getWebClient();
410         client.setWebConnection(webConnection);
411 
412         final HtmlPage page = client.getPage(URL_FIRST);
413         assertEquals("", page.getTitleText());
414 
415         startTimedTest();
416         assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(1_000));
417         assertMaxTestRunTime(1000);
418         assertTrue(page.getTitleText(), page.getTitleText().endsWith("work48§work49§work50§work51§"));
419     }
420 
421     /**
422      * {@link WebClient#waitForBackgroundJavaScript(long)} should have an overview of all windows.
423      * @throws Exception if the test fails
424      */
425     @Test
426     public void jobSchedulesJobInOtherWindow1() throws Exception {
427         final String html = DOCTYPE_HTML
428             + "<html>\n"
429             + "<head>\n"
430             + "  <script>\n"
431             + "    var counter = 0;\n"
432             + "    function test() {\n"
433             + "      var w = window.open('about:blank');\n"
434             + "      w.setTimeout(doWork1, 200);\n"
435             + "    }\n"
436             + "    function doWork1() {\n"
437             + "      alert('work1');\n"
438             + "      setTimeout(doWork2, 400);\n"
439             + "    }\n"
440             + "    function doWork2() {\n"
441             + "      alert('work2');\n"
442             + "    }\n"
443             + "  </script>\n"
444             + "</head>\n"
445             + "<body onload='test()'>\n"
446             + "</body>\n"
447             + "</html>";
448 
449         final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
450         final HtmlPage page = loadPage(html, collectedAlerts);
451 
452         startTimedTest();
453         assertEquals(0, page.getWebClient().waitForBackgroundJavaScript(1000));
454         assertMaxTestRunTime(1000);
455 
456         final String[] expectedAlerts = {"work1", "work2"};
457         assertEquals(expectedAlerts, collectedAlerts);
458     }
459 
460     /**
461      * {@link WebClient#waitForBackgroundJavaScriptStartingBefore(long)} should have an overview of all windows.
462      * @throws Exception if the test fails
463      */
464     @Test
465     public void jobSchedulesJobInOtherWindow2() throws Exception {
466         final String html = DOCTYPE_HTML
467             + "<html>\n"
468             + "<head>\n"
469             + "  <script>\n"
470             + LOG_TITLE_FUNCTION
471             + "    var counter = 0;\n"
472             + "    function test() {\n"
473             + "      var w = window.open('about:blank');\n"
474             + "      w.setTimeout(doWork1, 200);\n"
475             + "    }\n"
476             + "    function doWork1() {\n"
477             + "      log('work1');\n"
478             + "      setTimeout(doWork2, 400);\n"
479             + "    }\n"
480             + "    function doWork2() {\n"
481             + "      log('work2');\n"
482             + "    }\n"
483             + "  </script>\n"
484             + "</head>\n"
485             + "<body onload='test()'>\n"
486             + "</body>\n"
487             + "</html>";
488 
489         final HtmlPage page = loadPage(html);
490         assertEquals("", page.getTitleText());
491 
492         startTimedTest();
493         assertEquals(0, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(1000));
494         assertMaxTestRunTime(1000);
495 
496         assertEquals("work1§work2§", page.getTitleText());
497     }
498 
499     /**
500      * HtmlUnit-2.7-SNAPSHOT (as of 29.10.09) had bug with
501      * WebClient.waitForBackgroundJavaScriptStartingBefore: it could be totally blocking
502      * under some circumstances. This test reproduces the problem but ensures
503      * that the test terminates (calling clearInterval when waitForBackgroundJavaScriptStartingBefore
504      * has not done its job correctly).
505      * @throws Exception if the test fails
506      */
507     @RetryingTest(3)
508     public void waitForBackgroundJavaScriptStartingBefore_hangs() throws Exception {
509         final String html = DOCTYPE_HTML
510             + "<html>\n"
511             + "<head>\n"
512             + "  <title>test</title>\n"
513             + "  <script>\n"
514             + LOG_TITLE_FUNCTION
515             + "    var start = new Date().getTime();\n"
516             + "    var id = setInterval(doWork1, 35);\n"
517             + "    function stopTimer() {\n"
518             + "      clearInterval(id);\n"
519             + "    }\n"
520             + "    function doWork1() {\n"
521             + "      if (start + 8000 < new Date().getTime()) {\n"
522             + "        log('failed');\n"
523             + "        clearInterval(id);\n"
524             + "      }\n"
525             + "    }\n"
526             + "  </script>\n"
527             + "</head>\n"
528             + "<body>\n"
529             + "<button onclick='stopTimer()'>stop</button>\n"
530             + "</body>\n"
531             + "</html>";
532 
533         final WebClient client = getWebClient();
534 
535         final MockWebConnection webConnection = new MockWebConnection();
536         webConnection.setDefaultResponse(html);
537         client.setWebConnection(webConnection);
538 
539         final HtmlPage page = client.getPage(URL_FIRST);
540         assertEquals("test", page.getTitleText());
541 
542         int noOfJobs = client.waitForBackgroundJavaScriptStartingBefore(500);
543         assertTrue(noOfJobs == 1 || noOfJobs == 2); // maybe one is running
544         assertEquals("test", page.getTitleText());
545 
546         noOfJobs = client.waitForBackgroundJavaScriptStartingBefore(500);
547         assertTrue(noOfJobs == 1 || noOfJobs == 2); // maybe one is running
548         assertEquals("test", page.getTitleText());
549     }
550 
551     /**
552      * Methods waitForBackgroundJavaScript[StartingBefore] should not look for running jobs only on the existing
553      * windows but as well on the ones that have been (freshly) closed.
554      * This test shows the case where a background job in a frame causes the window of this frame to be unregistered
555      * by the WebClient but this job should still be considered until it completes.
556      * @throws Exception if the test fails
557      */
558     @Test
559     public void jobsFromAClosedWindowShouldntBeIgnore() throws Exception {
560         final String html = DOCTYPE_HTML
561             + "<html><head><title>page 1</title></head>\n"
562             + "<body>\n"
563             + "<iframe src='iframe.html'></iframe>\n"
564             + "</body></html>";
565 
566         final String iframe = DOCTYPE_HTML
567                 + "<html><body>\n"
568                 + "<script>\n"
569                 + "setTimeout(function() { parent.location = '/page3.html'; }, 50);\n"
570                 + "</script>\n"
571                 + "</body></html>";
572         final String page3 = DOCTYPE_HTML
573                 + "<html><body><script>\n"
574                 + "parent.location = '/delayedPage4.html';\n"
575                 + "</script></body></html>";
576 
577         final WebClient client = getWebClient();
578 
579         final ThreadSynchronizer threadSynchronizer = new ThreadSynchronizer();
580 
581         final MockWebConnection webConnection = new MockWebConnection() {
582             @Override
583             public WebResponse getResponse(final WebRequest request) throws IOException {
584                 if (request.getUrl().toExternalForm().endsWith("/delayedPage4.html")) {
585                     threadSynchronizer.setState("/delayedPage4.html requested");
586                     threadSynchronizer.waitForState("ready to call waitForBGJS");
587                     threadSynchronizer.sleep(1000);
588                 }
589                 return super.getResponse(request);
590             }
591         };
592 
593         webConnection.setDefaultResponse(html);
594         webConnection.setResponse(new URL(URL_FIRST, "iframe.html"), iframe);
595         webConnection.setResponse(new URL(URL_FIRST, "page3.html"), page3);
596         webConnection.setResponseAsGenericHtml(new URL(URL_FIRST, "delayedPage4.html"), "page 4");
597         client.setWebConnection(webConnection);
598 
599         client.getPage(URL_FIRST);
600 
601         threadSynchronizer.waitForState("/delayedPage4.html requested");
602         threadSynchronizer.setState("ready to call waitForBGJS");
603         final int noOfJobs = client.waitForBackgroundJavaScriptStartingBefore(500);
604         assertEquals(0, noOfJobs);
605 
606         final HtmlPage page = (HtmlPage) client.getCurrentWindow().getEnclosedPage();
607         assertEquals("page 4", page.getTitleText());
608     }
609 
610     /**
611      * @throws Exception if the test fails
612      */
613     @Test
614     public void timeoutBeforeAllStartedJobsFinished() throws Exception {
615         final String html = DOCTYPE_HTML
616                 + "<html>\n"
617                 + "<head>\n"
618                 + "</head>\n"
619                 + "<body>\n"
620                 + "  <script>\n"
621                 + LOG_TITLE_FUNCTION
622                 + "    var counter = 0;\n"
623                 + "    setTimeout(doWork1, 100);\n"
624                 + "    function doWork1() {\n"
625                 + "      log('work' + counter++);\n"
626                 + "      var start = new Date();\n"
627                 + "      while((new Date() - start) < 400) {"
628                 + "        Math.sqrt(Math.sqrt(Math.pi));\n"
629                 + "      }\n"
630                 + "      setTimeout(doWork1, 100);\n"
631                 + "    }\n"
632                 + "  </script>\n"
633                 + "</body>\n"
634                 + "</html>";
635 
636         final HtmlPage page = loadPage(html);
637         assertEquals("", page.getTitleText());
638 
639         startTimedTest();
640         assertEquals(1, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(650, 700));
641         assertMaxTestRunTime(800);
642 
643         assertTrue(page.getTitleText(), page.getTitleText().startsWith("work0§work1§"));
644     }
645 
646     /**
647      * @throws Exception if the test fails
648      */
649     @Test
650     public void timeoutDirectStartedJob() throws Exception {
651         final String html = DOCTYPE_HTML
652                 + "<html>\n"
653                 + "<head>\n"
654                 + "</head>\n"
655                 + "<body>\n"
656                 + "  <script>\n"
657                 + LOG_TITLE_FUNCTION
658                 + "    var counter = 0;\n"
659                 + "    function doWork1() {\n"
660                 + "      log('work' + counter++);\n"
661                 + "      var start = new Date();\n"
662                 + "      while((new Date() - start) < 2000) {"
663                 + "        Math.sqrt(Math.sqrt(Math.pi));\n"
664                 + "      }\n"
665                 + "    }\n"
666                 + "    setTimeout(doWork1, 100);\n"
667                 + "  </script>\n"
668                 + "</body>\n"
669                 + "</html>";
670 
671         final HtmlPage page = loadPage(html);
672         assertEquals("", page.getTitleText());
673 
674         startTimedTest();
675         assertEquals(1, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(400, 1000));
676         assertMaxTestRunTime(1100);
677 
678         assertTrue(page.getTitleText(), page.getTitleText().startsWith("work0§"));
679     }
680 
681     /**
682      * Helper to ensure some synchronization state between threads
683      * to reproduce a particular situation in the tests.
684      */
685     static class ThreadSynchronizer {
686         private String state_ = "initial";
687         private static final Log LOG = LogFactory.getLog(ThreadSynchronizer.class);
688 
689         synchronized void setState(final String newState) {
690             state_ = newState;
691             notifyAll();
692         }
693 
694         /**
695          * Just like {@link Thread#sleep(long)} but throws a {@link RuntimeException}.
696          * @param millis the time to sleep in milliseconds
697          */
698         public void sleep(final long millis) {
699             try {
700                 if (LOG.isDebugEnabled()) {
701                     LOG.debug("Sleeping for " + millis + "ms");
702                 }
703                 Thread.sleep(millis);
704             }
705             catch (final InterruptedException e) {
706                 throw new RuntimeException(e);
707             }
708         }
709 
710         synchronized void waitForState(final String expectedState) {
711             try {
712                 while (!state_.equals(expectedState)) {
713                     wait();
714                 }
715             }
716             catch (final InterruptedException e) {
717                 throw new RuntimeException(e);
718             }
719         }
720     }
721 }