Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dotnet] Gracefully handle clashing device names #14713

Merged
merged 9 commits into from
Nov 5, 2024
2 changes: 1 addition & 1 deletion dotnet/src/webdriver/Interactions/ActionSequence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public int Count
/// </summary>
public InputDevice inputDevice
nvborisenko marked this conversation as resolved.
Show resolved Hide resolved
{
get { return this.inputDevice; }
get { return this.device; }
}

/// <summary>
Expand Down
104 changes: 35 additions & 69 deletions dotnet/src/webdriver/Interactions/Actions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,23 @@ public class Actions : IAction
private PointerInputDevice activePointer;
private KeyInputDevice activeKeyboard;
private WheelInputDevice activeWheel;
private IActionExecutor actionExecutor;

/// <summary>
/// Initializes a new instance of the <see cref="Actions"/> class.
/// </summary>
/// <param name="driver">The <see cref="IWebDriver"/> object on which the actions built will be performed.</param>
/// <exception cref="ArgumentException">If <paramref name="driver"/> does not implement <see cref="IActionExecutor"/>.</exception>
public Actions(IWebDriver driver)
: this(driver, TimeSpan.FromMilliseconds(250))
{

}

/// <summary>
/// Initializes a new instance of the <see cref="Actions"/> class.
/// </summary>
/// <param name="driver">The <see cref="IWebDriver"/> object on which the actions built will be performed.</param>
/// <param name="duration">How long durable action is expected to take.</param>
/// <exception cref="ArgumentException">If <paramref name="driver"/> does not implement <see cref="IActionExecutor"/>.</exception>
public Actions(IWebDriver driver, TimeSpan duration)
{
IActionExecutor actionExecutor = GetDriverAs<IActionExecutor>(driver);
Expand All @@ -56,52 +56,33 @@ public Actions(IWebDriver driver, TimeSpan duration)
throw new ArgumentException("The IWebDriver object must implement or wrap a driver that implements IActionExecutor.", nameof(driver));
}

this.actionExecutor = actionExecutor;
this.ActionExecutor = actionExecutor;

this.duration = duration;
}

/// <summary>
/// Returns the <see cref="IActionExecutor"/> for the driver.
/// </summary>
protected IActionExecutor ActionExecutor
{
get { return this.actionExecutor; }
}
protected IActionExecutor ActionExecutor { get; }

/// <summary>
/// Sets the active pointer device for this Actions class.
/// </summary>
/// <param name="kind">The kind of pointer device to set as active.</param>
/// <param name="name">The name of the pointer device to set as active.</param>
/// <returns>A self-reference to this Actions class.</returns>
/// <exception cref="InvalidOperationException">If a device with this name exists but is not a pointer.</exception>
public Actions SetActivePointer(PointerKind kind, string name)
{
IList<ActionSequence> sequences = this.actionBuilder.ToActionSequenceList();
InputDevice device = FindDeviceById(name);

InputDevice device = null;

foreach (var sequence in sequences)
this.activePointer = device switch
{
Dictionary<string, object> actions = sequence.ToDictionary();

string id = (string)actions["id"];

if (id == name)
{
device = sequence.inputDevice;
break;
}
}

if (device == null)
{
this.activePointer = new PointerInputDevice(kind, name);
}
else
{
this.activePointer = (PointerInputDevice)device;
}
null => new PointerInputDevice(kind, name),
PointerInputDevice pointerDevice => pointerDevice,
_ => throw new InvalidOperationException($"Device under the name \"{name}\" is not a pointer. Actual input type: {device.DeviceKind}"),
};

return this;
}
Expand All @@ -111,33 +92,17 @@ public Actions SetActivePointer(PointerKind kind, string name)
/// </summary>
/// <param name="name">The name of the keyboard device to set as active.</param>
/// <returns>A self-reference to this Actions class.</returns>
/// <exception cref="InvalidOperationException">If a device with this name exists but is not a keyboard.</exception>
public Actions SetActiveKeyboard(string name)
{
IList<ActionSequence> sequences = this.actionBuilder.ToActionSequenceList();
InputDevice device = FindDeviceById(name);

InputDevice device = null;

foreach (var sequence in sequences)
this.activeKeyboard = device switch
{
Dictionary<string, object> actions = sequence.ToDictionary();

string id = (string)actions["id"];

if (id == name)
{
device = sequence.inputDevice;
break;
}
}

if (device == null)
{
this.activeKeyboard = new KeyInputDevice(name);
}
else
{
this.activeKeyboard = (KeyInputDevice)device;
}
null => new KeyInputDevice(name),
KeyInputDevice keyDevice => keyDevice,
_ => throw new InvalidOperationException($"Device under the name \"{name}\" is not a keyboard. Actual input type: {device.DeviceKind}"),
};

return this;
}
Expand All @@ -147,35 +112,36 @@ public Actions SetActiveKeyboard(string name)
/// </summary>
/// <param name="name">The name of the wheel device to set as active.</param>
/// <returns>A self-reference to this Actions class.</returns>
/// <exception cref="InvalidOperationException">If a device with this name exists but is not a wheel.</exception>
public Actions SetActiveWheel(string name)
{
IList<ActionSequence> sequences = this.actionBuilder.ToActionSequenceList();
InputDevice device = FindDeviceById(name);

this.activeWheel = device switch
{
null => new WheelInputDevice(name),
WheelInputDevice wheelDevice => wheelDevice,
_ => throw new InvalidOperationException($"Device under the name \"{name}\" is not a wheel. Actual input type: {device.DeviceKind}"),
};

InputDevice device = null;
return this;
}

foreach (var sequence in sequences)
private InputDevice FindDeviceById(string name)
nvborisenko marked this conversation as resolved.
Show resolved Hide resolved
{
foreach (var sequence in this.actionBuilder.ToActionSequenceList())
{
Dictionary<string, object> actions = sequence.ToDictionary();

string id = (string)actions["id"];

if (id == name)
{
device = sequence.inputDevice;
break;
return sequence.inputDevice;
}
}

if (device == null)
{
this.activeWheel = new WheelInputDevice(name);
}
else
{
this.activeWheel = (WheelInputDevice)device;
}

return this;
return null;
}

/// <summary>
Expand Down Expand Up @@ -619,7 +585,7 @@ public IAction Build()
/// </summary>
public void Perform()
{
this.actionExecutor.PerformActions(this.actionBuilder.ToActionSequenceList());
this.ActionExecutor.PerformActions(this.actionBuilder.ToActionSequenceList());
this.actionBuilder.ClearSequences();
}

Expand Down
31 changes: 31 additions & 0 deletions dotnet/test/common/Interactions/CombinedInputActionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,37 @@ public void PerformsPause()
Assert.IsTrue(DateTime.Now - start > TimeSpan.FromMilliseconds(1200));
}

[Test]
public void ShouldHandleClashingDeviceNamesGracefully()
{
var actionsWithPointer = new Actions(driver)
.SetActivePointer(PointerKind.Mouse, "test")
.Click();

Assert.That(() =>
{
actionsWithPointer.SetActiveWheel("test");
}, Throws.InvalidOperationException.With.Message.EqualTo("Device under the name \"test\" is not a wheel. Actual input type: Pointer"));

var actionsWithKeyboard = new Actions(driver)
.SetActiveKeyboard("test")
.KeyDown(Keys.Shift).KeyUp(Keys.Shift);

Assert.That(() =>
{
actionsWithKeyboard.SetActivePointer(PointerKind.Pen, "test");
}, Throws.InvalidOperationException.With.Message.EqualTo("Device under the name \"test\" is not a pointer. Actual input type: Key"));

var actionsWithWheel = new Actions(driver)
.SetActiveWheel("test")
.ScrollByAmount(0, 0);

Assert.That(() =>
{
actionsWithWheel.SetActiveKeyboard("test");
}, Throws.InvalidOperationException.With.Message.EqualTo("Device under the name \"test\" is not a keyboard. Actual input type: Wheel"));
}

private bool FuzzyPositionMatching(int expectedX, int expectedY, string locationTuple)
{
string[] splitString = locationTuple.Split(',');
Expand Down
Loading