blob: 9fd21d633f89858f249ee7e0a84b47cbd0cfc440 [file] [log] [blame]
// <copyright file="AsyncJavaScriptExecutor.cs" company="WebDriver Committers">
// Copyright 2007-2011 WebDriver committers
// Copyright 2007-2011 Google Inc.
// Portions copyright 2011 Software Freedom Conservancy
//
// 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.
// </copyright>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
namespace OpenQA.Selenium.Internal
{
/// <summary>
/// Utility class used to execute "asynchronous" scripts. This class should
/// only be used by browsers that do not natively support asynchronous
/// script execution.
/// <para>Warning: this class is intended for internal use
/// only. This class will be removed without warning after all
/// native asynchronous implementations have been completed.
/// </para>
/// </summary>
public class AsyncJavaScriptExecutor
{
private const string AsyncScriptTemplate = @"document.__$webdriverPageId = '{0}';
var timeoutId = window.setTimeout(function() {{
window.setTimeout(function() {{
document.__$webdriverAsyncTimeout = 1;
}}, 0);
}}, {1});
document.__$webdriverAsyncTimeout = 0;
var callback = function(value) {{
document.__$webdriverAsyncTimeout = 0;
document.__$webdriverAsyncScriptResult = value;
window.clearTimeout(timeoutId);
}};
var argsArray = Array.prototype.slice.call(arguments);
argsArray.push(callback);
if (document.__$webdriverAsyncScriptResult !== undefined) {{
delete document.__$webdriverAsyncScriptResult;
}}
(function() {{
{2}
}}).apply(null, argsArray);";
private const string PollingScriptTemplate = @"var pendingId = '{0}';
if (document.__$webdriverPageId != '{1}') {{
return [pendingId, -1];
}} else if ('__$webdriverAsyncScriptResult' in document) {{
var value = document.__$webdriverAsyncScriptResult;
delete document.__$webdriverAsyncScriptResult;
return value;
}} else {{
return [pendingId, document.__$webdriverAsyncTimeout];
}}
";
private IJavaScriptExecutor executor;
private TimeSpan timeout = TimeSpan.FromMilliseconds(0);
/// <summary>
/// Initializes a new instance of the <see cref="AsyncJavaScriptExecutor"/> class.
/// </summary>
/// <param name="executor">An <see cref="IJavaScriptExecutor"/> object capable of executing JavaScript.</param>
public AsyncJavaScriptExecutor(IJavaScriptExecutor executor)
{
this.executor = executor;
}
/// <summary>
/// Gets or sets the timeout for the script executor.
/// </summary>
public TimeSpan Timeout
{
get { return this.timeout; }
set { this.timeout = value; }
}
/// <summary>
/// Executes a JavaScript script asynchronously.
/// </summary>
/// <param name="script">The script to execute.</param>
/// <param name="args">An array of objects used as arguments in the script.</param>
/// <returns>The object which is the return value of the script.</returns>
/// <exception cref="InvalidOperationException">if the object executing the function doesn't support JavaScript.</exception>
/// <exception cref="WebDriverException">if the page reloads during the JavaScript execution.</exception>
/// <exception cref="WebDriverTimeoutException">if the timeout expires during the JavaScript execution.</exception>
public object ExecuteScript(string script, object[] args)
{
// Injected into the page along with the user's script. Used to detect when a new page is
// loaded while waiting for the script result.
string pageId = Guid.NewGuid().ToString();
string asyncScript = string.Format(CultureInfo.InvariantCulture, AsyncScriptTemplate, pageId, this.timeout.TotalMilliseconds, script);
// This is used by our polling function to return a result that indicates the script has
// neither finished nor timed out yet.
string pendingId = Guid.NewGuid().ToString();
string pollFunction = string.Format(CultureInfo.InvariantCulture, PollingScriptTemplate, pendingId, pageId);
// Execute the async script.
DateTime startTime = DateTime.Now;
this.executor.ExecuteScript(asyncScript, args);
// Finally, enter a loop running the poll function. This loop will run until one of the
// following occurs:
// - The async script invokes the callback with its result.
// - The poll function detects that the script has timed out.
// We rely on the polling function to detect timeouts so we stay in sync with the browser's
// javascript event loop.
while (true)
{
object result = this.executor.ExecuteScript(pollFunction);
ReadOnlyCollection<object> resultList = result as ReadOnlyCollection<object>;
if (resultList != null && resultList.Count == 2 && pendingId == resultList[0].ToString())
{
long timeoutFlag = (long)resultList[1];
if (timeoutFlag < 0)
{
throw new WebDriverException(
"Detected a new page load while waiting for async script result."
+ "\nScript: " + script);
}
TimeSpan elapsedTime = DateTime.Now - startTime;
if (timeoutFlag > 0)
{
throw new WebDriverTimeoutException("Timed out waiting for async script callback."
+ "\nElapsed time: " + elapsedTime.Milliseconds + "ms"
+ "\nScript: " + script);
}
}
else
{
return result;
}
System.Threading.Thread.Sleep(100);
}
}
}
}