Java tutorial
/* * Copyright (c) 2002-2016 Gargoyle Software Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gargoylesoftware.htmlunit; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.Test; import org.junit.runner.RunWith; import com.gargoylesoftware.htmlunit.BrowserRunner.Tries; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJobManager; /** * Tests for {@link WebClient#waitForBackgroundJavaScriptStartingBefore(long)} and * {@link WebClient#waitForBackgroundJavaScript(long)}. * * @author Marc Guillemot */ @RunWith(BrowserRunner.class) public class WebClientWaitForBackgroundJobsTest extends SimpleWebTestCase { private static String XHRInstantiation_ = "(window.XMLHttpRequest ? " + "new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'))"; private long startTime_; private void startTimedTest() { startTime_ = System.currentTimeMillis(); } private void assertMaxTestRunTime(final long maxRunTimeMilliseconds) { final long endTime = System.currentTimeMillis(); final long runTime = endTime - startTime_; assertTrue( "\nTest took too long to run and results may not be accurate. Please try again. " + "\n Actual Run Time: " + runTime + "\n Max Run Time: " + maxRunTimeMilliseconds, runTime < maxRunTimeMilliseconds); } /** * @throws Exception if the test fails */ @Test public void dontWaitWhenUnnecessary() throws Exception { final String content = "<html>\n" + "<head>\n" + " <title>test</title>\n" + " <script>\n" + " var threadID;\n" + " function test() {\n" + " threadID = setTimeout(doAlert, 10000);\n" + " }\n" + " function doAlert() {\n" + " alert('blah');\n" + " }\n" + " </script>\n" + "</head>\n" + "<body onload='test()'>\n" + "</body>\n" + "</html>"; final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>()); final HtmlPage page = loadPage(content, collectedAlerts); final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager(); assertNotNull(jobManager); assertEquals(1, jobManager.getJobCount()); startTimedTest(); assertEquals(1, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(7000)); assertMaxTestRunTime(100); assertEquals(1, jobManager.getJobCount()); assertEquals(Collections.EMPTY_LIST, collectedAlerts); } /** * @throws Exception if the test fails */ @Test @Tries(3) public void dontWaitWhenUnnecessary_jobRemovesOtherJob() throws Exception { final String content = "<html>\n" + "<head>\n" + " <title>test</title>\n" + " <script>\n" + " var longTimeoutID;\n" + " function test() {\n" + " longTimeoutID = setTimeout(doAlert('late'), 10000);\n" + " setTimeout(clearLongTimeout, 100);\n" + " setTimeout(doAlert('hello'), 300);\n" + " }\n" + " function clearLongTimeout() {\n" + " alert('clearLongTimeout');\n" + " clearTimeout(longTimeoutID);\n" + " }\n" + " function doAlert(_text) {\n" + " return function doAlert() {\n" + " alert(_text);\n" + " }\n" + " }\n" + " </script>\n" + "</head>\n" + "<body onload='test()'>\n" + "</body>\n" + "</html>"; final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>()); final HtmlPage page = loadPage(content, collectedAlerts); final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager(); assertNotNull(jobManager); assertEquals(3, jobManager.getJobCount()); startTimedTest(); assertEquals(0, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(20_000)); assertMaxTestRunTime(400); assertEquals(0, jobManager.getJobCount()); final String[] expectedAlerts = { "clearLongTimeout", "hello" }; assertEquals(expectedAlerts, collectedAlerts); } /** * When waitForBackgroundJavaScriptStartingBefore is called while a job is being executed, it has * to wait for this job to finish, even if this clearXXX has been called for it. * @throws Exception if the test fails */ @Test @Tries(3) public void waitCalledDuringJobExecution() throws Exception { final String html = "<html>\n" + "<head>\n" + " <title>test</title>\n" + " <script>\n" + " var intervalId;\n" + " function test() {\n" + " intervalId = setTimeout(doWork, 100);\n" + " }\n" + " function doWork() {\n" + " clearTimeout(intervalId);\n" + " // waitForBackgroundJavaScriptStartingBefore should be called when JS execution is here\n" + " var request = " + XHRInstantiation_ + ";\n" + " request.open('GET', 'wait', false);\n" + " request.send('');\n" + " alert('end work');\n" + " }\n" + " </script>\n" + "</head>\n" + "<body onload='test()'>\n" + "</body>\n" + "</html>"; final ThreadSynchronizer threadSynchronizer = new ThreadSynchronizer(); final MockWebConnection webConnection = new MockWebConnection() { @Override public WebResponse getResponse(final WebRequest request) throws IOException { if (request.getUrl().toExternalForm().endsWith("/wait")) { threadSynchronizer.waitForState("just before waitForBackgroundJavaScriptStartingBefore"); threadSynchronizer.sleep(400); // main thread need to be able to process next instruction } return super.getResponse(request); } }; webConnection.setResponse(URL_FIRST, html); webConnection.setDefaultResponse(""); final WebClient client = getWebClient(); client.setWebConnection(webConnection); final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>()); client.setAlertHandler(new CollectingAlertHandler(collectedAlerts)); final HtmlPage page = client.getPage(URL_FIRST); final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager(); assertNotNull(jobManager); assertEquals(1, jobManager.getJobCount()); startTimedTest(); threadSynchronizer.setState("just before waitForBackgroundJavaScriptStartingBefore"); assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(20_000)); assertMaxTestRunTime(600); assertEquals(0, jobManager.getJobCount()); final String[] expectedAlerts = { "end work" }; assertEquals(expectedAlerts, collectedAlerts); } /** * When waitForBackgroundJavaScriptStartingBefore is called and a new job is scheduled after the one that * is first found as the last one within the delay (a job starts a new one or simply a setInterval), * a few retries should be done to see if new jobs exists. * @throws Exception if the test fails */ @Test public void waitWhenLastJobStartsNewOne() throws Exception { final String html = "<html>\n" + "<head>\n" + " <title>test</title>\n" + " <script>\n" + " function test() {\n" + " setTimeout(doWork1, 200);\n" + " }\n" + " function doWork1() {\n" + " alert('work1');\n" + " setTimeout(doWork2, 200);\n" + " }\n" + " function doWork2() {\n" + " alert('work2');\n" + " }\n" + " </script>\n" + "</head>\n" + "<body onload='test()'>\n" + "</body>\n" + "</html>"; final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>()); final HtmlPage page = loadPage(html, collectedAlerts); final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager(); assertNotNull(jobManager); assertEquals(1, jobManager.getJobCount()); startTimedTest(); assertEquals(0, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(20_000)); assertMaxTestRunTime(1000); assertEquals(0, jobManager.getJobCount()); final String[] expectedAlerts = { "work1", "work2" }; assertEquals(expectedAlerts, collectedAlerts); } /** * When waitForBackgroundJavaScriptStartingBefore is called and a new job is scheduled after the one that * is first found as the last one within the delay (a job starts a new one or simply a setInterval), * a few retries should be done to see if new jobs exists. * @throws Exception if the test fails */ @Test @Tries(3) public void waitWithsubWindows() throws Exception { final String html = "<html>\n" + "<head>\n" + " <title>test</title>\n" + "</head>\n" + "<body>\n" + "<iframe src='nested.html'></iframe>\n" + "</body>\n" + "</html>"; final String nested = "<html>\n" + "<head>\n" + " <title>nested</title>\n" + " <script>\n" + " function test() {\n" + " setTimeout(doWork1, 200);\n" + " }\n" + " function doWork1() {\n" + " alert('work1');\n" + " }\n" + " </script>\n" + "</head>\n" + "<body onload='test()'>\n" + "</body>\n" + "</html>"; final WebClient client = getWebClient(); final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>()); client.setAlertHandler(new CollectingAlertHandler(collectedAlerts)); final MockWebConnection webConnection = new MockWebConnection(); webConnection.setResponse(URL_FIRST, html); webConnection.setDefaultResponse(nested); client.setWebConnection(webConnection); final HtmlPage page = client.getPage(URL_FIRST); final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager(); assertNotNull(jobManager); assertEquals(0, jobManager.getJobCount()); startTimedTest(); assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(20_000)); assertMaxTestRunTime(300); assertEquals(0, jobManager.getJobCount()); final String[] expectedAlerts = { "work1" }; assertEquals(expectedAlerts, collectedAlerts); } /** * Test the case where a job is being executed at the time where waitForBackgroundJavaScriptStartingBefore * and where this jobs schedules a new job after call to waitForBackgroundJavaScriptStartingBefore, * . * @throws Exception if the test fails */ @Test @Tries(3) public void newJobStartedAfterWait() throws Exception { final String html = "<html>\n" + "<head>\n" + " <title>test</title>\n" + " <script>\n" + " var request;\n" + " function onReadyStateChange() {\n" + " if (request.readyState == 4) {\n" + " alert('xhr onchange');\n" + " setTimeout(doWork1, 200);\n" + " }\n" + " }\n" + " function test() {\n" + " request = " + XHRInstantiation_ + ";\n" + " request.open('GET', 'wait', true);\n" + " request.onreadystatechange = onReadyStateChange;\n" + " // waitForBackgroundJavaScriptStartingBefore should be called when JS execution is in send()\n" + " request.send('');\n" + " }\n" + " function doWork1() {\n" + " alert('work1');\n" + " }\n" + " </script>\n" + "</head>\n" + "<body onload='test()'>\n" + "</body>\n" + "</html>"; final ThreadSynchronizer threadSynchronizer = new ThreadSynchronizer(); final MockWebConnection webConnection = new MockWebConnection() { @Override public WebResponse getResponse(final WebRequest request) throws IOException { if (request.getUrl().toExternalForm().endsWith("/wait")) { threadSynchronizer.waitForState("just before waitForBackgroundJavaScriptStartingBefore"); threadSynchronizer.sleep(400); // main thread need to be able to process next instruction } return super.getResponse(request); } }; webConnection.setResponse(URL_FIRST, html); webConnection.setDefaultResponse(""); final WebClient client = getWebClient(); client.setWebConnection(webConnection); final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>()); client.setAlertHandler(new CollectingAlertHandler(collectedAlerts)); final HtmlPage page = client.getPage(URL_FIRST); final JavaScriptJobManager jobManager = page.getEnclosingWindow().getJobManager(); assertNotNull(jobManager); assertEquals(1, jobManager.getJobCount()); startTimedTest(); threadSynchronizer.setState("just before waitForBackgroundJavaScriptStartingBefore"); assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(20_000)); assertMaxTestRunTime(1000); assertEquals(0, jobManager.getJobCount()); final String[] expectedAlerts = { "xhr onchange", "work1" }; assertEquals(expectedAlerts, collectedAlerts); } /** * Tests that waitForBackgroundJavaScriptStartingBefore waits for jobs that should have been started earlier * but that are "late" due to processing of previous job. * This test needs to start many setTimeout to expect to reach the state, where a check for future * jobs occurs when one of this job is not active. * @throws Exception if the test fails */ @Test @Tries(3) public void waitForJobThatIsAlreadyLate() throws Exception { final String html = "<html>\n" + "<head>\n" + " <script>\n" + " var counter = 0;\n" + " function test() {\n" + " setTimeout(doWork1, 0);\n" + " }\n" + " function doWork1() {\n" + " if (counter++ < 50) {\n" + " setTimeout(doWork1, 0);\n" + " }\n" + " alert('work1');\n" + " }\n" + " </script>\n" + "</head>\n" + "<body onload='test()'>\n" + "</body>\n" + "</html>"; final MockWebConnection webConnection = new MockWebConnection(); webConnection.setResponse(URL_FIRST, html); webConnection.setDefaultResponse(""); final WebClient client = getWebClient(); client.setWebConnection(webConnection); final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>()); client.setAlertHandler(new CollectingAlertHandler(collectedAlerts)); client.getPage(URL_FIRST); startTimedTest(); assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(1000)); assertMaxTestRunTime(1000); assertEquals(51, collectedAlerts.size()); } /** * {@link WebClient#waitForBackgroundJavaScript(long)} should have an overview of all windows. * @throws Exception if the test fails */ @Test public void jobSchedulesJobInOtherWindow1() throws Exception { final String html = "<html>\n" + "<head>\n" + " <script>\n" + " var counter = 0;\n" + " function test() {\n" + " var w = window.open('about:blank');\n" + " w.setTimeout(doWork1, 200);\n" + " }\n" + " function doWork1() {\n" + " alert('work1');\n" + " setTimeout(doWork2, 400);\n" + " }\n" + " function doWork2() {\n" + " alert('work2');\n" + " }\n" + " </script>\n" + "</head>\n" + "<body onload='test()'>\n" + "</body>\n" + "</html>"; final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>()); final HtmlPage page = loadPage(html, collectedAlerts); startTimedTest(); assertEquals(0, page.getWebClient().waitForBackgroundJavaScript(1000)); assertMaxTestRunTime(1000); final String[] expectedAlerts = { "work1", "work2" }; assertEquals(expectedAlerts, collectedAlerts); } /** * {@link WebClient#waitForBackgroundJavaScriptStartingBefore(long)} should have an overview of all windows. * @throws Exception if the test fails */ @Test public void jobSchedulesJobInOtherWindow2() throws Exception { final String html = "<html>\n" + "<head>\n" + " <script>\n" + " var counter = 0;\n" + " function test() {\n" + " var w = window.open('about:blank');\n" + " w.setTimeout(doWork1, 200);\n" + " }\n" + " function doWork1() {\n" + " alert('work1');\n" + " setTimeout(doWork2, 400);\n" + " }\n" + " function doWork2() {\n" + " alert('work2');\n" + " }\n" + " </script>\n" + "</head>\n" + "<body onload='test()'>\n" + "</body>\n" + "</html>"; final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>()); final HtmlPage page = loadPage(html, collectedAlerts); startTimedTest(); assertEquals(0, page.getWebClient().waitForBackgroundJavaScriptStartingBefore(1000)); assertMaxTestRunTime(1000); final String[] expectedAlerts = { "work1", "work2" }; assertEquals(expectedAlerts, collectedAlerts); } /** * HtmlUnit-2.7-SNAPSHOT (as of 29.10.09) had bug with * WebClient.waitForBackgroundJavaScriptStartingBefore: it could be totally blocking * under some circumstances. This test reproduces the problem but ensures * that the test terminates (calling clearInterval when waitForBackgroundJavaScriptStartingBefore * has not done its job correctly). * @throws Exception if the test fails */ @Test @Tries(3) public void waitForBackgroundJavaScriptStartingBefore_hangs() throws Exception { final String html = "<html>\n" + "<head>\n" + " <title>test</title>\n" + " <script>\n" + " var start = new Date().getTime();\n" + " var id = setInterval(doWork1, 35);\n" + " function stopTimer() {\n" + " clearInterval(id);\n" + " }\n" + " function doWork1() {\n" + " if (start + 8000 < new Date().getTime()) {\n" + " document.title = 'failed';\n" + " clearInterval(id);\n" + " }\n" + " }\n" + " </script>\n" + "</head>\n" + "<body>\n" + "<button onclick='stopTimer()'>stop</button>\n" + "</body>\n" + "</html>"; final WebClient client = getWebClient(); final MockWebConnection webConnection = new MockWebConnection(); webConnection.setDefaultResponse(html); client.setWebConnection(webConnection); final HtmlPage page = client.getPage(URL_FIRST); int noOfJobs = client.waitForBackgroundJavaScriptStartingBefore(500); assertTrue(noOfJobs == 1 || noOfJobs == 2); // maybe one is running assertEquals("test", page.getTitleText()); noOfJobs = client.waitForBackgroundJavaScriptStartingBefore(500); assertTrue(noOfJobs == 1 || noOfJobs == 2); // maybe one is running } /** * Methods waitForBackgroundJavaScript[StartingBefore] should not look for running jobs only on the existing * windows but as well on the ones that have been (freshly) closed. * This test shows the case where a background job in a frame causes the window of this frame to be unregistered * by the WebClient but this job should still be considered until it completes. * @throws Exception if the test fails */ @Test public void jobsFromAClosedWindowShouldntBeIgnore() throws Exception { final String html = "<html><head><title>page 1</title></head>\n" + "<body>\n" + "<iframe src='iframe.html'></iframe>\n" + "</body></html>"; final String iframe = "<html><body>\n" + "<script>\n" + "setTimeout(function() { parent.location = '/page3.html'; }, 50);\n" + "</script>\n" + "</body></html>"; final String page3 = "<html><body><script>\n" + "parent.location = '/delayedPage4.html';\n" + "</script></body></html>"; final WebClient client = getWebClient(); final ThreadSynchronizer threadSynchronizer = new ThreadSynchronizer(); final MockWebConnection webConnection = new MockWebConnection() { @Override public WebResponse getResponse(final WebRequest request) throws IOException { if (request.getUrl().toExternalForm().endsWith("/delayedPage4.html")) { threadSynchronizer.setState("/delayedPage4.html requested"); threadSynchronizer.waitForState("ready to call waitForBGJS"); threadSynchronizer.sleep(1000); } return super.getResponse(request); } }; webConnection.setDefaultResponse(html); webConnection.setResponse(new URL(getDefaultUrl(), "iframe.html"), iframe); webConnection.setResponse(new URL(getDefaultUrl(), "page3.html"), page3); webConnection.setResponseAsGenericHtml(new URL(getDefaultUrl(), "delayedPage4.html"), "page 4"); client.setWebConnection(webConnection); client.getPage(getDefaultUrl()); threadSynchronizer.waitForState("/delayedPage4.html requested"); threadSynchronizer.setState("ready to call waitForBGJS"); final int noOfJobs = client.waitForBackgroundJavaScriptStartingBefore(500); assertEquals(0, noOfJobs); final HtmlPage page = (HtmlPage) client.getCurrentWindow().getEnclosedPage(); assertEquals("page 4", page.getTitleText()); } } /** * Helper to ensure some synchronization state between threads to reproduce a particular situation in the tests. * @author Marc Guillemot */ class ThreadSynchronizer { private String state_ = "initial"; private static final Log LOG = LogFactory.getLog(ThreadSynchronizer.class); synchronized void setState(final String newState) { state_ = newState; notifyAll(); } /** * Just like {@link Thread#sleep(long)} but throws a {@link RuntimeException}. * @param millis the time to sleep in milliseconds */ public void sleep(final long millis) { try { if (LOG.isDebugEnabled()) { LOG.debug("Sleeping for " + millis + "ms"); } Thread.sleep(millis); } catch (final InterruptedException e) { throw new RuntimeException(e); } } synchronized void waitForState(final String expectedState) { try { while (!state_.equals(expectedState)) { wait(); } } catch (final InterruptedException e) { throw new RuntimeException(e); } } }