Skip to content

Commit

Permalink
[dotnet] Annotate nullability on Actions type (#15208)
Browse files Browse the repository at this point in the history
* [dotnet] Annotate nullability on `Actions` type

* Add `Keys` nullability
  • Loading branch information
RenderMichael authored Feb 1, 2025
1 parent 01a96d9 commit 5b648f3
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 29 deletions.
74 changes: 48 additions & 26 deletions dotnet/src/webdriver/Interactions/Actions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

#nullable enable

namespace OpenQA.Selenium.Interactions
{
Expand All @@ -29,9 +32,9 @@ public class Actions : IAction
{
private readonly TimeSpan duration;
private ActionBuilder actionBuilder = new ActionBuilder();
private PointerInputDevice activePointer;
private KeyInputDevice activeKeyboard;
private WheelInputDevice activeWheel;
private PointerInputDevice? activePointer;
private KeyInputDevice? activeKeyboard;
private WheelInputDevice? activeWheel;

/// <summary>
/// Initializes a new instance of the <see cref="Actions"/> class.
Expand All @@ -51,14 +54,10 @@ public Actions(IWebDriver driver)
/// <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);
if (actionExecutor == null)
{
throw new ArgumentException("The IWebDriver object must implement or wrap a driver that implements IActionExecutor.", nameof(driver));
}
IActionExecutor actionExecutor = GetDriverAs<IActionExecutor>(driver)
?? throw new ArgumentException("The IWebDriver object must implement or wrap a driver that implements IActionExecutor.", nameof(driver));

this.ActionExecutor = actionExecutor;

this.duration = duration;
}

Expand All @@ -74,9 +73,10 @@ public Actions(IWebDriver driver, TimeSpan duration)
/// <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>
[MemberNotNull(nameof(activePointer))]
public Actions SetActivePointer(PointerKind kind, string name)
{
InputDevice device = FindDeviceById(name);
InputDevice? device = FindDeviceById(name);

this.activePointer = device switch
{
Expand All @@ -94,9 +94,10 @@ public Actions SetActivePointer(PointerKind kind, string name)
/// <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>
[MemberNotNull(nameof(activeKeyboard))]
public Actions SetActiveKeyboard(string name)
{
InputDevice device = FindDeviceById(name);
InputDevice? device = FindDeviceById(name);

this.activeKeyboard = device switch
{
Expand All @@ -114,9 +115,10 @@ public Actions SetActiveKeyboard(string name)
/// <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>
[MemberNotNull(nameof(activeWheel))]
public Actions SetActiveWheel(string name)
{
InputDevice device = FindDeviceById(name);
InputDevice? device = FindDeviceById(name);

this.activeWheel = device switch
{
Expand All @@ -128,7 +130,7 @@ public Actions SetActiveWheel(string name)
return this;
}

private InputDevice FindDeviceById(string name)
private InputDevice? FindDeviceById(string? name)
{
foreach (var sequence in this.actionBuilder.ToActionSequenceList())
{
Expand Down Expand Up @@ -211,14 +213,14 @@ public Actions KeyDown(string theKey)
/// of <see cref="Keys.Shift"/>, <see cref="Keys.Control"/>, <see cref="Keys.Alt"/>,
/// <see cref="Keys.Meta"/>, <see cref="Keys.Command"/>,<see cref="Keys.LeftAlt"/>,
/// <see cref="Keys.LeftControl"/>,<see cref="Keys.LeftShift"/>.</exception>
public Actions KeyDown(IWebElement element, string theKey)
public Actions KeyDown(IWebElement? element, string theKey)
{
if (string.IsNullOrEmpty(theKey))
{
throw new ArgumentException("The key value must not be null or empty", nameof(theKey));
}

ILocatable target = GetLocatableFromElement(element);
ILocatable? target = GetLocatableFromElement(element);
if (element != null)
{
this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerMove(element, 0, 0, duration));
Expand Down Expand Up @@ -255,14 +257,14 @@ public Actions KeyUp(string theKey)
/// of <see cref="Keys.Shift"/>, <see cref="Keys.Control"/>, <see cref="Keys.Alt"/>,
/// <see cref="Keys.Meta"/>, <see cref="Keys.Command"/>,<see cref="Keys.LeftAlt"/>,
/// <see cref="Keys.LeftControl"/>,<see cref="Keys.LeftShift"/>.</exception>
public Actions KeyUp(IWebElement element, string theKey)
public Actions KeyUp(IWebElement? element, string theKey)
{
if (string.IsNullOrEmpty(theKey))
{
throw new ArgumentException("The key value must not be null or empty", nameof(theKey));
}

ILocatable target = GetLocatableFromElement(element);
ILocatable? target = GetLocatableFromElement(element);
if (element != null)
{
this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerMove(element, 0, 0, duration));
Expand All @@ -279,6 +281,7 @@ public Actions KeyUp(IWebElement element, string theKey)
/// </summary>
/// <param name="keysToSend">The keystrokes to send to the browser.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentException">If <paramref name="keysToSend"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
public Actions SendKeys(string keysToSend)
{
return this.SendKeys(null, keysToSend);
Expand All @@ -290,14 +293,15 @@ public Actions SendKeys(string keysToSend)
/// <param name="element">The element to which to send the keystrokes.</param>
/// <param name="keysToSend">The keystrokes to send to the browser.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
public Actions SendKeys(IWebElement element, string keysToSend)
/// <exception cref="ArgumentException">If <paramref name="keysToSend"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
public Actions SendKeys(IWebElement? element, string keysToSend)
{
if (string.IsNullOrEmpty(keysToSend))
{
throw new ArgumentException("The key value must not be null or empty", nameof(keysToSend));
}

ILocatable target = GetLocatableFromElement(element);
ILocatable? target = GetLocatableFromElement(element);
if (element != null)
{
this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerMove(element, 0, 0, duration));
Expand All @@ -319,6 +323,7 @@ public Actions SendKeys(IWebElement element, string keysToSend)
/// </summary>
/// <param name="onElement">The element on which to click and hold.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
public Actions ClickAndHold(IWebElement onElement)
{
this.MoveToElement(onElement).ClickAndHold();
Expand All @@ -340,6 +345,7 @@ public Actions ClickAndHold()
/// </summary>
/// <param name="onElement">The element on which to release the button.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
public Actions Release(IWebElement onElement)
{
this.MoveToElement(onElement).Release();
Expand All @@ -361,6 +367,7 @@ public Actions Release()
/// </summary>
/// <param name="onElement">The element on which to click.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
public Actions Click(IWebElement onElement)
{
this.MoveToElement(onElement).Click();
Expand All @@ -383,6 +390,7 @@ public Actions Click()
/// </summary>
/// <param name="onElement">The element on which to double-click.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
public Actions DoubleClick(IWebElement onElement)
{
this.MoveToElement(onElement).DoubleClick();
Expand All @@ -407,6 +415,7 @@ public Actions DoubleClick()
/// </summary>
/// <param name="toElement">The element to which to move the mouse.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="toElement"/> is null.</exception>
public Actions MoveToElement(IWebElement toElement)
{
if (toElement == null)
Expand Down Expand Up @@ -460,6 +469,7 @@ public Actions MoveToLocation(int offsetX, int offsetY)
/// </summary>
/// <param name="onElement">The element on which to right-click.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
public Actions ContextClick(IWebElement onElement)
{
this.MoveToElement(onElement).ContextClick();
Expand All @@ -483,6 +493,7 @@ public Actions ContextClick()
/// <param name="source">The element on which the drag operation is started.</param>
/// <param name="target">The element on which the drop is performed.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="source"/> or <paramref name="target"/> are null.</exception>
public Actions DragAndDrop(IWebElement source, IWebElement target)
{
this.ClickAndHold(source).MoveToElement(target).Release(target);
Expand All @@ -496,6 +507,7 @@ public Actions DragAndDrop(IWebElement source, IWebElement target)
/// <param name="offsetX">The horizontal offset to which to move the mouse.</param>
/// <param name="offsetY">The vertical offset to which to move the mouse.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="source"/> is null.</exception>
public Actions DragAndDropToOffset(IWebElement source, int offsetX, int offsetY)
{
this.ClickAndHold(source).MoveByOffset(offsetX, offsetY).Release();
Expand All @@ -507,6 +519,7 @@ public Actions DragAndDropToOffset(IWebElement source, int offsetX, int offsetY)
/// </summary>
/// <param name="element">Which element to scroll into the viewport.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="element"/> is null.</exception>
public Actions ScrollToElement(IWebElement element)
{
this.actionBuilder.AddAction(this.GetActiveWheel().CreateWheelScroll(element, 0, 0, 0, 0, duration));
Expand Down Expand Up @@ -540,8 +553,15 @@ public Actions ScrollByAmount(int deltaX, int deltaY)
/// <param name="deltaY">Distance along Y axis to scroll using the wheel. A negative value scrolls up.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="MoveTargetOutOfBoundsException">If the origin with offset is outside the viewport.</exception>
/// <exception cref="ArgumentNullException">If <paramref name="scrollOrigin"/> is null.</exception>
/// <exception cref="ArgumentException">If both or either of Viewport and Element are set.</exception>
public Actions ScrollFromOrigin(WheelInputDevice.ScrollOrigin scrollOrigin, int deltaX, int deltaY)
{
if (scrollOrigin is null)
{
throw new ArgumentNullException(nameof(scrollOrigin));
}

if (scrollOrigin.Viewport && scrollOrigin.Element != null)
{
throw new ArgumentException("viewport can not be true if an element is defined.", nameof(scrollOrigin));
Expand All @@ -554,7 +574,7 @@ public Actions ScrollFromOrigin(WheelInputDevice.ScrollOrigin scrollOrigin, int
}
else
{
this.actionBuilder.AddAction(this.GetActiveWheel().CreateWheelScroll(scrollOrigin.Element,
this.actionBuilder.AddAction(this.GetActiveWheel().CreateWheelScroll(scrollOrigin.Element!,
scrollOrigin.XOffset, scrollOrigin.YOffset, deltaX, deltaY, duration));
}

Expand All @@ -566,6 +586,7 @@ public Actions ScrollFromOrigin(WheelInputDevice.ScrollOrigin scrollOrigin, int
/// </summary>
/// <param name="duration">How long to pause the action chain.</param>
/// <returns>A self-reference to this <see cref="Actions"/>.</returns>
/// <exception cref="ArgumentException">If <paramref name="duration"/> is negative.</exception>
public Actions Pause(TimeSpan duration)
{
this.actionBuilder.AddAction(new PauseInteraction(this.GetActivePointer(), duration));
Expand Down Expand Up @@ -603,15 +624,16 @@ public void Reset()
/// </summary>
/// <param name="element">The <see cref="IWebElement"/> to get the location of.</param>
/// <returns>The <see cref="ILocatable"/> of the <see cref="IWebElement"/>.</returns>
protected static ILocatable GetLocatableFromElement(IWebElement element)
[return: NotNullIfNotNull(nameof(element))]
protected static ILocatable? GetLocatableFromElement(IWebElement? element)
{
if (element == null)
{
return null;
}

ILocatable target = null;
IWrapsElement wrapper = element as IWrapsElement;
ILocatable? target = null;
IWrapsElement? wrapper = element as IWrapsElement;
while (wrapper != null)
{
target = wrapper.WrappedElement as ILocatable;
Expand All @@ -631,12 +653,12 @@ protected static ILocatable GetLocatableFromElement(IWebElement element)
return target;
}

private T GetDriverAs<T>(IWebDriver driver) where T : class
private static T? GetDriverAs<T>(IWebDriver? driver) where T : class
{
T driverAsType = driver as T;
T? driverAsType = driver as T;
if (driverAsType == null)
{
IWrapsDriver wrapper = driver as IWrapsDriver;
IWrapsDriver? wrapper = driver as IWrapsDriver;
while (wrapper != null)
{
driverAsType = wrapper.WrappedDriver as T;
Expand Down
1 change: 1 addition & 0 deletions dotnet/src/webdriver/Interactions/InputDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public Interaction CreatePause()
/// of the pause. Note that <see cref="TimeSpan.Zero"/> pauses to synchronize
/// with other action sequences for other devices.</param>
/// <returns>The <see cref="Interaction"/> representing the action.</returns>
/// <exception cref="ArgumentException">If <paramref name="duration"/> is negative.</exception>
public Interaction CreatePause(TimeSpan duration)
{
return new PauseInteraction(this, duration);
Expand Down
1 change: 1 addition & 0 deletions dotnet/src/webdriver/Interactions/PauseInteraction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public PauseInteraction(InputDevice sourceDevice)
/// </summary>
/// <param name="sourceDevice">The input device on which to execute the pause.</param>
/// <param name="duration">The length of time to pause for.</param>
/// <exception cref="ArgumentException">If <paramref name="duration"/> is negative.</exception>
public PauseInteraction(InputDevice sourceDevice, TimeSpan duration)
: base(sourceDevice)
{
Expand Down
9 changes: 6 additions & 3 deletions dotnet/src/webdriver/Keys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
using System.Collections.Generic;
using System.Globalization;

#nullable enable

namespace OpenQA.Selenium
{
/// <summary>
Expand Down Expand Up @@ -352,13 +354,14 @@ public static class Keys
/// </summary>
public static readonly string ZenkakuHankaku = Convert.ToString(Convert.ToChar(0xE040, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);

private static Dictionary<string, string> descriptions;
private static Dictionary<string, string>? descriptions;

/// <summary>
/// Gets the description of a specific key.
/// </summary>
/// <param name="value">The key value for which to get the description.</param>
/// <returns>The description of the key.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="value"/> is <see langword="null"/>.</exception>
internal static object GetDescription(string value)
{
if (descriptions == null)
Expand Down Expand Up @@ -423,9 +426,9 @@ internal static object GetDescription(string value)
descriptions.Add(ZenkakuHankaku, "Zenkaku Hankaku");
}

if (descriptions.ContainsKey(value))
if (descriptions.TryGetValue(value, out string? description))
{
return descriptions[value];
return description;
}

return value;
Expand Down

0 comments on commit 5b648f3

Please sign in to comment.