Skip to content

Commit 72760b0

Browse files
liviocjimevans
authored andcommitted
Added an API to execute native PhantomJS code in .NET
Fixes issue #6949.
1 parent 45478af commit 72760b0

File tree

8 files changed

+259
-37
lines changed

8 files changed

+259
-37
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<html>
2+
<head>
3+
<title>random numbers</title>
4+
<script>
5+
function display() {
6+
document.getElementById('numbers').textContent = numbers.join(' ');
7+
}
8+
</script>
9+
</head>
10+
<body onload="display()">
11+
<p>Some random numbers between 0 and 100:</p>
12+
13+
<!-- Placeholder for the list of random numbers -->
14+
<p id="numbers"></p>
15+
<script>
16+
numbers = [];
17+
while (numbers.length < 100) {
18+
numbers.push(Math.round(Math.random() * 100));
19+
}
20+
</script>
21+
</body>
22+
</html>

dotnet/CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ v2.40.0
3939
collisions in random number generation encountered when using the .NET
4040
Random class, particularly in different processes. GUIDs are used as their
4141
generation in the .NET Framework is optimized for performance.
42+
* Updated .NET PhantomJS driver to be able to execute code in the PhantomJS
43+
context. This allows the user to execute PhantomJS-specific commands using
44+
the PhantomJS JavaScript API.
4245
* Updated .NET bindings to use webdriver.json for default Firefox profile.
4346
Other language bindings have been using a common JSON file containing the
4447
default profile setting for the anonymous Firefox profile used by the

dotnet/src/webdriver/PhantomJS/PhantomJSDriver.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
using System.Globalization;
2222
using System.Text;
2323
using OpenQA.Selenium.Remote;
24+
using OpenQA.Selenium.Internal;
2425

2526
namespace OpenQA.Selenium.PhantomJS
2627
{
@@ -64,6 +65,11 @@ namespace OpenQA.Selenium.PhantomJS
6465
/// </example>
6566
public class PhantomJSDriver : RemoteWebDriver, ITakesScreenshot
6667
{
68+
/// <summary>
69+
/// Command name of the PhantomJS-specific command to execute native script in PhantomJS.
70+
/// </summary>
71+
private const string CommandExecutePhantomScript = "executePhantomScript";
72+
6773
/// <summary>
6874
/// Initializes a new instance of the <see cref="PhantomJSDriver"/> class.
6975
/// </summary>
@@ -143,6 +149,9 @@ public PhantomJSDriver(PhantomJSDriverService service, PhantomJSOptions options)
143149
public PhantomJSDriver(PhantomJSDriverService service, PhantomJSOptions options, TimeSpan commandTimeout)
144150
: base(new DriverServiceCommandExecutor(service, commandTimeout, false), options.ToCapabilities())
145151
{
152+
// Add the custom commandInfo of PhantomJSDriver
153+
CommandInfo commandInfo = new CommandInfo(CommandInfo.PostCommand, "/session/{sessionId}/phantom/execute");
154+
CommandInfoRepository.Instance.TryAddAdditionalCommand(CommandExecutePhantomScript, commandInfo);
146155
}
147156

148157
/// <summary>
@@ -161,6 +170,39 @@ public override IFileDetector FileDetector
161170
set { }
162171
}
163172

173+
/// <summary>
174+
/// Execute a PhantomJS script fragment. Provides extra functionality not found in WebDriver
175+
/// but available in PhantomJS.
176+
/// </summary>
177+
/// <param name="script">The fragment of PhantomJS JavaScript to execute.</param>
178+
/// <param name="args">List of arguments to pass to the function that the script is wrapped in.
179+
/// These can accessed in the script as 'arguments[0]', 'arguments[1]','arguments[2]', etc
180+
/// </param>
181+
/// <returns>The result of the evaluation.</returns>
182+
/// <remarks>
183+
/// <para>
184+
/// See the <a href="https://github.com/ariya/phantomjs/wiki/API-Reference">PhantomJS API</a>
185+
/// for details on what is available.
186+
/// </para>
187+
/// <para>
188+
/// A 'page' variable pointing to currently selected page is available for use.
189+
/// If there is no page yet, one is created.
190+
/// </para>
191+
/// <para>
192+
/// When overriding any callbacks be sure to wrap in a try/catch block, as failures
193+
/// may cause future WebDriver calls to fail.
194+
/// </para>
195+
/// <para>
196+
/// Certain callbacks are used by GhostDriver (the PhantomJS WebDriver implementation)
197+
/// already. Overriding these may cause the script to fail. It's a good idea to check
198+
/// for existing callbacks before overriding.
199+
/// </para>
200+
/// </remarks>
201+
public object ExecutePhantomJS(string script, params object[] args)
202+
{
203+
return this.ExecuteScriptCommand(script, CommandExecutePhantomScript, args);
204+
}
205+
164206
#region ITakesScreenshot Members
165207
/// <summary>
166208
/// Gets a <see cref="Screenshot"/> object representing the image of the page on the screen.

dotnet/src/webdriver/Remote/CommandInfoRepository.cs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,19 @@ namespace OpenQA.Selenium.Remote
2424
{
2525
/// <summary>
2626
/// Holds the information about all commands specified by the JSON wire protocol.
27+
/// This class cannot be inherited, as it is intended to be a singleton, and
28+
/// allowing subclasses introduces the possibility of multiple instances.
2729
/// </summary>
28-
public class CommandInfoRepository
30+
public sealed class CommandInfoRepository
2931
{
3032
#region Private members
31-
private static object lockObject = new object();
32-
private static CommandInfoRepository collectionInstance;
33+
private static readonly object lockObject = new object();
3334

34-
private Dictionary<string, CommandInfo> commandDictionary;
35+
// Note this is marked volatile to force the runtime to allow
36+
// serialized access to the value.
37+
private static volatile CommandInfoRepository collectionInstance;
38+
39+
private readonly Dictionary<string, CommandInfo> commandDictionary;
3540
#endregion
3641

3742
#region Constructor
@@ -53,11 +58,16 @@ public static CommandInfoRepository Instance
5358
{
5459
get
5560
{
56-
lock (lockObject)
61+
// This is an implementation using double-checked locking as described
62+
// at http://msdn.microsoft.com/en-us/library/ff650316.aspx
63+
if (collectionInstance == null)
5764
{
58-
if (collectionInstance == null)
65+
lock (lockObject)
5966
{
60-
collectionInstance = new CommandInfoRepository();
67+
if (collectionInstance == null)
68+
{
69+
collectionInstance = new CommandInfoRepository();
70+
}
6171
}
6272
}
6373

@@ -82,6 +92,38 @@ public CommandInfo GetCommandInfo(string commandName)
8292

8393
return toReturn;
8494
}
95+
96+
/// <summary>
97+
/// Tries the add an additional command to the list of known commands.
98+
/// </summary>
99+
/// <param name="commandName">Name of the command.</param>
100+
/// <param name="commandInfo">The command information.</param>
101+
/// <returns><see langword="true"/> if the new command has been added successfully; otherwise, <see langword="false"/>.</returns>
102+
/// <remarks>
103+
/// This method is used by WebDriver implementations to add additional custom driver-specific commands.
104+
/// This method will not overwrite existing commands for a specific name.
105+
/// </remarks>
106+
internal bool TryAddAdditionalCommand(string commandName, CommandInfo commandInfo)
107+
{
108+
if (string.IsNullOrEmpty(commandName))
109+
{
110+
throw new ArgumentNullException("commandName", "The name of the command cannot be null or the empty string.");
111+
}
112+
113+
if (commandInfo == null)
114+
{
115+
throw new ArgumentNullException("commandInfo", "The command information object cannot be null.");
116+
}
117+
118+
if (this.commandDictionary.ContainsKey(commandName))
119+
{
120+
return false;
121+
}
122+
123+
this.commandDictionary.Add(commandName, commandInfo);
124+
return true;
125+
}
126+
85127
#endregion
86128

87129
#region Private support methods

dotnet/src/webdriver/Remote/RemoteWebDriver.cs

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ public ITargetLocator SwitchTo()
423423
/// <returns>The value returned by the script.</returns>
424424
public object ExecuteScript(string script, params object[] args)
425425
{
426-
return this.ExecuteScriptInternal(script, false, args);
426+
return this.ExecuteScriptCommand(script, DriverCommand.ExecuteScript, args);
427427
}
428428

429429
/// <summary>
@@ -434,7 +434,7 @@ public object ExecuteScript(string script, params object[] args)
434434
/// <returns>The value returned by the script.</returns>
435435
public object ExecuteAsyncScript(string script, params object[] args)
436436
{
437-
return this.ExecuteScriptInternal(script, true, args);
437+
return this.ExecuteScriptCommand(script, DriverCommand.ExecuteAsyncScript, args);
438438
}
439439
#endregion
440440

@@ -903,6 +903,33 @@ protected virtual RemoteWebElement CreateElement(string elementId)
903903
RemoteWebElement toReturn = new RemoteWebElement(this, elementId);
904904
return toReturn;
905905
}
906+
907+
protected object ExecuteScriptCommand(string script, string commandName, params object[] args)
908+
{
909+
if (!this.Capabilities.IsJavaScriptEnabled)
910+
{
911+
throw new NotSupportedException("You must be using an underlying instance of WebDriver that supports executing javascript");
912+
}
913+
914+
// Escape the quote marks
915+
// script = script.Replace("\"", "\\\"");
916+
object[] convertedArgs = ConvertArgumentsToJavaScriptObjects(args);
917+
918+
Dictionary<string, object> parameters = new Dictionary<string, object>();
919+
parameters.Add("script", script);
920+
921+
if (convertedArgs != null && convertedArgs.Length > 0)
922+
{
923+
parameters.Add("args", convertedArgs);
924+
}
925+
else
926+
{
927+
parameters.Add("args", new object[] { });
928+
}
929+
930+
Response commandResponse = this.Execute(commandName, parameters);
931+
return this.ParseJavaScriptReturnValue(commandResponse.Value);
932+
}
906933
#endregion
907934

908935
#region Private methods
@@ -962,6 +989,11 @@ private static object ConvertObjectToJavaScriptObject(object arg)
962989
return converted;
963990
}
964991

992+
/// <summary>
993+
/// Converts the arguments to java script objects.
994+
/// </summary>
995+
/// <param name="args">The arguments.</param>
996+
/// <returns></returns>
965997
private static object[] ConvertArgumentsToJavaScriptObjects(object[] args)
966998
{
967999
if (args == null)
@@ -1060,34 +1092,6 @@ private static void UnpackAndThrowOnError(Response errorResponse)
10601092
}
10611093
}
10621094

1063-
private object ExecuteScriptInternal(string script, bool async, params object[] args)
1064-
{
1065-
if (!this.Capabilities.IsJavaScriptEnabled)
1066-
{
1067-
throw new NotSupportedException("You must be using an underlying instance of WebDriver that supports executing javascript");
1068-
}
1069-
1070-
// Escape the quote marks
1071-
// script = script.Replace("\"", "\\\"");
1072-
object[] convertedArgs = ConvertArgumentsToJavaScriptObjects(args);
1073-
1074-
Dictionary<string, object> parameters = new Dictionary<string, object>();
1075-
parameters.Add("script", script);
1076-
1077-
if (convertedArgs != null && convertedArgs.Length > 0)
1078-
{
1079-
parameters.Add("args", convertedArgs);
1080-
}
1081-
else
1082-
{
1083-
parameters.Add("args", new object[] { });
1084-
}
1085-
1086-
string command = async ? DriverCommand.ExecuteAsyncScript : DriverCommand.ExecuteScript;
1087-
Response commandResponse = this.Execute(command, parameters);
1088-
return this.ParseJavaScriptReturnValue(commandResponse.Value);
1089-
}
1090-
10911095
private object ParseJavaScriptReturnValue(object responseValue)
10921096
{
10931097
object returnValue = null;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using NUnit.Framework;
2+
using OpenQA.Selenium.Environment;
3+
4+
namespace OpenQA.Selenium.PhantomJS
5+
{
6+
[SetUpFixture]
7+
// Outside a namespace to affect the entire assembly
8+
public class MySetUpClass
9+
{
10+
[SetUp]
11+
public void RunBeforeAnyTest()
12+
{
13+
EnvironmentManager.Instance.WebServer.Start();
14+
}
15+
16+
[TearDown]
17+
public void RunAfterAnyTests()
18+
{
19+
EnvironmentManager.Instance.CloseCurrentDriver();
20+
EnvironmentManager.Instance.WebServer.Stop();
21+
}
22+
}
23+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using NUnit.Framework;
6+
using OpenQA.Selenium.Environment;
7+
8+
namespace OpenQA.Selenium.PhantomJS
9+
{
10+
[TestFixture]
11+
public class PhantomJSSpecificTests : DriverTestFixture
12+
{
13+
[Test]
14+
public void ShouldBeAbleToReturnResultsFromScriptExecutedInPhantomJSContext()
15+
{
16+
if (!(driver is PhantomJSDriver))
17+
{
18+
// Skip this test if not using PhantomJS.
19+
// The command under test is only available when using PhantomJS
20+
return;
21+
}
22+
23+
PhantomJSDriver phantom = (PhantomJSDriver)driver;
24+
25+
// Do we get results back?
26+
object result = phantom.ExecutePhantomJS("return 1 + 1");
27+
Assert.AreEqual(2L, (long)result);
28+
}
29+
30+
[Test]
31+
public void ShouldBeAbleToReadArgumentsInScriptExecutedInPhantomJSContext()
32+
{
33+
if (!(driver is PhantomJSDriver))
34+
{
35+
// Skip this test if not using PhantomJS.
36+
// The command under test is only available when using PhantomJS
37+
return;
38+
}
39+
40+
PhantomJSDriver phantom = (PhantomJSDriver)driver;
41+
42+
// Can we read arguments?
43+
object result = phantom.ExecutePhantomJS("return arguments[0] + arguments[0]", 1L);
44+
Assert.AreEqual(2L, (long)result);
45+
}
46+
47+
[Test]
48+
public void ShouldBeAbleToOverrideScriptInPageFromPhantomJSContext()
49+
{
50+
if (!(driver is PhantomJSDriver))
51+
{
52+
// Skip this test if not using PhantomJS.
53+
// The command under test is only available when using PhantomJS
54+
return;
55+
}
56+
57+
PhantomJSDriver phantom = (PhantomJSDriver)driver;
58+
59+
// Can we override some browser JavaScript functions in the page context?
60+
object result = phantom.ExecutePhantomJS("var page = this;" +
61+
"page.onInitialized = function () { " +
62+
"page.evaluate(function () { " +
63+
"Math.random = function() { return 42 / 100 } " +
64+
"})" +
65+
"}");
66+
67+
phantom.Url = EnvironmentManager.Instance.UrlBuilder.WhereIs("injectableContent.html");
68+
IWebElement numbers = phantom.FindElement(By.Id("numbers"));
69+
bool foundAtLeastOne = false;
70+
foreach (string number in numbers.Text.Split(' '))
71+
{
72+
foundAtLeastOne = true;
73+
Assert.AreEqual("42", number);
74+
}
75+
76+
Assert.IsTrue(foundAtLeastOne);
77+
}
78+
}
79+
}
80+

0 commit comments

Comments
 (0)