From 2ea99a35e8bf3e087063962474b10e29596229d8 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Tue, 4 Feb 2025 02:38:31 -0500 Subject: [PATCH 1/6] [dotnet] Annotate nullability on more of `WebElement` --- dotnet/src/webdriver/WebElement.cs | 61 +++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/dotnet/src/webdriver/WebElement.cs b/dotnet/src/webdriver/WebElement.cs index 0c0eca538b109..3fe3ce5ded52c 100644 --- a/dotnet/src/webdriver/WebElement.cs +++ b/dotnet/src/webdriver/WebElement.cs @@ -40,6 +40,8 @@ public class WebElement : IWebElement, IFindsElement, IWrapsDriver, ILocatable, /// public const string ElementReferencePropertyName = "element-6066-11e4-a52e-4f735466cecf"; +#nullable enable + private readonly WebDriver driver; /// @@ -59,6 +61,8 @@ public WebElement(WebDriver parentDriver, string id) /// public IWebDriver WrappedDriver => this.driver; +#nullable restore + /// /// Gets the tag name of this element. /// @@ -99,6 +103,8 @@ public virtual string Text } } +#nullable enable + /// /// Gets a value indicating whether or not this element is enabled. /// @@ -151,7 +157,11 @@ public virtual Point Location Response commandResponse = this.Execute(DriverCommand.GetElementRect, parameters); - Dictionary rawPoint = (Dictionary)commandResponse.Value; + if (commandResponse.Value is not Dictionary rawPoint) + { + throw new WebDriverException($"GetElementRect command was successful, but response was not an object: {commandResponse.Value}"); + } + int x = Convert.ToInt32(rawPoint["x"], CultureInfo.InvariantCulture); int y = Convert.ToInt32(rawPoint["y"], CultureInfo.InvariantCulture); return new Point(x, y); @@ -171,7 +181,11 @@ public virtual Size Size Response commandResponse = this.Execute(DriverCommand.GetElementRect, parameters); - Dictionary rawSize = (Dictionary)commandResponse.Value; + if (commandResponse.Value is not Dictionary rawSize) + { + throw new WebDriverException($"GetElementRect command was successful, but response was not an object: {commandResponse.Value}"); + } + int width = Convert.ToInt32(rawSize["width"], CultureInfo.InvariantCulture); int height = Convert.ToInt32(rawSize["height"], CultureInfo.InvariantCulture); return new Size(width, height); @@ -207,7 +221,7 @@ public virtual Point LocationOnScreenOnceScrolledIntoView { get { - object scriptResponse = this.driver.ExecuteScript("var rect = arguments[0].getBoundingClientRect(); return {'x': rect.left, 'y': rect.top};", this); + object scriptResponse = this.driver.ExecuteScript("var rect = arguments[0].getBoundingClientRect(); return {'x': rect.left, 'y': rect.top};", this)!; Dictionary rawLocation = (Dictionary)scriptResponse; @@ -217,6 +231,8 @@ public virtual Point LocationOnScreenOnceScrolledIntoView } } +#nullable restore + /// /// Gets the computed accessible label of this element. /// @@ -253,6 +269,8 @@ public virtual string ComputedAccessibleRole } } +#nullable enable + /// /// Gets the coordinates identifying the location of this element using /// various frames of reference. @@ -318,6 +336,7 @@ public virtual void Click() /// /// The locating mechanism to use. /// The first matching on the current context. + /// If is . /// If no element matches the criteria. public virtual IWebElement FindElement(By by) { @@ -329,6 +348,8 @@ public virtual IWebElement FindElement(By by) return by.FindElement(this); } +#nullable restore + /// /// Finds a child element matching the given mechanism and value. /// @@ -382,6 +403,8 @@ public virtual ReadOnlyCollection FindElements(string mechanism, st return this.driver.GetElementsFromResponse(commandResponse); } +#nullable enable + /// /// Gets the value of the specified attribute or property for this element. /// @@ -419,15 +442,14 @@ public virtual ReadOnlyCollection FindElements(string mechanism, st /// via JavaScript. /// /// Thrown when the target element is no longer valid in the document DOM. - public virtual string GetAttribute(string attributeName) + public virtual string? GetAttribute(string attributeName) { - Response commandResponse = null; - string attributeValue = string.Empty; Dictionary parameters = new Dictionary(); string atom = GetAtom("get-attribute.js"); parameters.Add("script", atom); parameters.Add("args", new object[] { ((IWebDriverObjectReference)this).ToDictionary(), attributeName }); - commandResponse = this.Execute(DriverCommand.ExecuteScript, parameters); + + Response commandResponse = Execute(DriverCommand.ExecuteScript, parameters); // Normalize string values of boolean results as lowercase. @@ -452,7 +474,7 @@ public virtual string GetAttribute(string attributeName) /// of an IDL property of the element, either use the /// method or the method. /// - public virtual string GetDomAttribute(string attributeName) + public virtual string? GetDomAttribute(string attributeName) { Dictionary parameters = new Dictionary(); parameters.Add("id", this.Id); @@ -470,7 +492,7 @@ public virtual string GetDomAttribute(string attributeName) /// The JavaScript property's current value. Returns a if the /// value is not set or the property does not exist. /// Thrown when the target element is no longer valid in the document DOM. - public virtual string GetDomProperty(string propertyName) + public virtual string? GetDomProperty(string propertyName) { Dictionary parameters = new Dictionary(); parameters.Add("id", this.Id); @@ -493,12 +515,12 @@ public virtual ISearchContext GetShadowRoot() parameters.Add("id", this.Id); Response commandResponse = this.Execute(DriverCommand.GetElementShadowRoot, parameters); - if (commandResponse.Value is not Dictionary shadowRootDictionary) + if (commandResponse.Value is not Dictionary shadowRootDictionary) { throw new WebDriverException("Get shadow root command succeeded, but response value does not represent a shadow root."); } - if (!ShadowRoot.TryCreate(this.driver, shadowRootDictionary, out ShadowRoot shadowRoot)) + if (!ShadowRoot.TryCreate(this.driver, shadowRootDictionary, out ShadowRoot? shadowRoot)) { throw new WebDriverException("Get shadow root command succeeded, but response value does not have a shadow root key value."); } @@ -506,6 +528,8 @@ public virtual ISearchContext GetShadowRoot() return shadowRoot; } +#nullable restore + /// /// Gets the value of a CSS property of this element. /// @@ -524,9 +548,12 @@ public virtual string GetCssValue(string propertyName) parameters.Add("name", propertyName); Response commandResponse = this.Execute(DriverCommand.GetElementValueOfCssProperty, parameters); + return commandResponse.Value.ToString(); } +#nullable enable + /// /// Gets a object representing the image of this element on the screen. /// @@ -538,7 +565,7 @@ public virtual Screenshot GetScreenshot() // Get the screenshot as base64. Response screenshotResponse = this.Execute(DriverCommand.ElementScreenshot, parameters); - string base64 = screenshotResponse.Value.ToString(); + string base64 = screenshotResponse.Value?.ToString() ?? throw new WebDriverException("ElementScreenshot command returned successfully, but with no response"); // ... and convert it. return new Screenshot(base64); @@ -597,7 +624,7 @@ public virtual void SendKeys(string text) /// Thrown when the target element is no longer valid in the document DOM. public virtual void Submit() { - string elementType = this.GetAttribute("type"); + string? elementType = this.GetAttribute("type"); if (elementType != null && elementType == "submit") { this.Click(); @@ -641,7 +668,7 @@ public override int GetHashCode() /// /// Object to compare against /// A boolean if it is equal or not - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is not IWebElement other) { @@ -676,6 +703,8 @@ Dictionary IWebDriverObjectReference.ToDictionary() return elementDictionary; } +#nullable restore + /// /// Executes a command on this element using the specified parameters. /// @@ -687,6 +716,8 @@ protected virtual Response Execute(string commandToExecute, Dictionary Date: Tue, 4 Feb 2025 14:09:31 -0500 Subject: [PATCH 2/6] Handle nullability of GetCssValue --- dotnet/src/webdriver/WebElement.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dotnet/src/webdriver/WebElement.cs b/dotnet/src/webdriver/WebElement.cs index 3fe3ce5ded52c..a586b8ae83bae 100644 --- a/dotnet/src/webdriver/WebElement.cs +++ b/dotnet/src/webdriver/WebElement.cs @@ -528,8 +528,6 @@ public virtual ISearchContext GetShadowRoot() return shadowRoot; } -#nullable restore - /// /// Gets the value of a CSS property of this element. /// @@ -549,10 +547,13 @@ public virtual string GetCssValue(string propertyName) Response commandResponse = this.Execute(DriverCommand.GetElementValueOfCssProperty, parameters); - return commandResponse.Value.ToString(); - } + if (commandResponse.Value is null) + { + throw new WebDriverException("GetElementValueOfCssProperty command returned a successful result, but contained no data"); + } -#nullable enable + return commandResponse.Value.ToString()!; + } /// /// Gets a object representing the image of this element on the screen. From e8c53affcd7c3316b38b0882f88672fc132a8918 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Tue, 4 Feb 2025 14:25:39 -0500 Subject: [PATCH 3/6] Implement nullability on more of `WebElement` --- dotnet/src/webdriver/WebElement.cs | 48 +++++++++++++++++++----------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/dotnet/src/webdriver/WebElement.cs b/dotnet/src/webdriver/WebElement.cs index a586b8ae83bae..d3f14c191c0d7 100644 --- a/dotnet/src/webdriver/WebElement.cs +++ b/dotnet/src/webdriver/WebElement.cs @@ -61,8 +61,6 @@ public WebElement(WebDriver parentDriver, string id) /// public IWebDriver WrappedDriver => this.driver; -#nullable restore - /// /// Gets the tag name of this element. /// @@ -81,7 +79,11 @@ public virtual string TagName Response commandResponse = this.Execute(DriverCommand.GetElementTagName, parameters); - return commandResponse.Value.ToString(); + if (commandResponse.Value is not Dictionary rawSize) + { + throw new WebDriverException($"GetElementTagName command was successful, but response was not an object: {commandResponse.Value}"); + } + return commandResponse.Value.ToString()!; } } @@ -99,12 +101,15 @@ public virtual string Text Response commandResponse = this.Execute(DriverCommand.GetElementText, parameters); - return commandResponse.Value.ToString(); + if (commandResponse.Value is not Dictionary rawSize) + { + throw new WebDriverException($"GetElementText command was successful, but response was not an object: {commandResponse.Value}"); + } + + return commandResponse.Value.ToString()!; } } -#nullable enable - /// /// Gets a value indicating whether or not this element is enabled. /// @@ -231,8 +236,6 @@ public virtual Point LocationOnScreenOnceScrolledIntoView } } -#nullable restore - /// /// Gets the computed accessible label of this element. /// @@ -245,7 +248,12 @@ public virtual string ComputedAccessibleLabel Response commandResponse = this.Execute(DriverCommand.GetComputedAccessibleLabel, parameters); - return commandResponse.Value.ToString(); + if (commandResponse.Value is null) + { + throw new WebDriverException("GetComputedAccessibleLabel command returned a successful result, but contained no data"); + } + + return commandResponse.Value.ToString()!; } } @@ -256,21 +264,21 @@ public virtual string ComputedAccessibleRole { get { - // TODO: Returning this as a string is incorrect. The W3C WebDriver Specification - // needs to be updated to more thoroughly document the structure of what is returned - // by this command. Once that is done, a type-safe class will be created, and will - // be returned by this property. Dictionary parameters = new Dictionary(); parameters.Add("id", this.Id); Response commandResponse = this.Execute(DriverCommand.GetComputedAccessibleRole, parameters); +#nullable disable + // TODO: Returning this as a string is incorrect. The W3C WebDriver Specification + // needs to be updated to more thoroughly document the structure of what is returned + // by this command. Once that is done, a type-safe class will be created, and will + // be returned by this property. return commandResponse.Value.ToString(); +#nullable enable } } -#nullable enable - /// /// Gets the coordinates identifying the location of this element using /// various frames of reference. @@ -331,6 +339,8 @@ public virtual void Click() this.Execute(DriverCommand.ClickElement, parameters); } +#nullable restore + /// /// Finds the first using the given method. /// @@ -348,7 +358,7 @@ public virtual IWebElement FindElement(By by) return by.FindElement(this); } -#nullable restore +#nullable enable /// /// Finds a child element matching the given mechanism and value. @@ -368,6 +378,8 @@ public virtual IWebElement FindElement(string mechanism, string value) return this.driver.GetElementFromResponse(commandResponse); } +#nullable restore + /// /// Finds all IWebElements within the current context /// using the given mechanism. @@ -385,6 +397,8 @@ public virtual ReadOnlyCollection FindElements(By by) return by.FindElements(this); } +#nullable enable + /// /// Finds all child elements matching the given mechanism and value. /// @@ -403,8 +417,6 @@ public virtual ReadOnlyCollection FindElements(string mechanism, st return this.driver.GetElementsFromResponse(commandResponse); } -#nullable enable - /// /// Gets the value of the specified attribute or property for this element. /// From bca2dc8a7c3ac9d0ffb3453dbdd55126f72a4113 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Tue, 4 Feb 2025 14:26:48 -0500 Subject: [PATCH 4/6] fix typo --- dotnet/src/webdriver/WebElement.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/webdriver/WebElement.cs b/dotnet/src/webdriver/WebElement.cs index d3f14c191c0d7..805dd0a625e8d 100644 --- a/dotnet/src/webdriver/WebElement.cs +++ b/dotnet/src/webdriver/WebElement.cs @@ -79,7 +79,7 @@ public virtual string TagName Response commandResponse = this.Execute(DriverCommand.GetElementTagName, parameters); - if (commandResponse.Value is not Dictionary rawSize) + if (commandResponse.Value is null) { throw new WebDriverException($"GetElementTagName command was successful, but response was not an object: {commandResponse.Value}"); } @@ -101,7 +101,7 @@ public virtual string Text Response commandResponse = this.Execute(DriverCommand.GetElementText, parameters); - if (commandResponse.Value is not Dictionary rawSize) + if (commandResponse.Value is null) { throw new WebDriverException($"GetElementText command was successful, but response was not an object: {commandResponse.Value}"); } From c55eb00ed708e24f1be351255b1cb739a66a7cec Mon Sep 17 00:00:00 2001 From: Michael Render Date: Tue, 4 Feb 2025 14:27:59 -0500 Subject: [PATCH 5/6] improve error message --- dotnet/src/webdriver/WebElement.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/webdriver/WebElement.cs b/dotnet/src/webdriver/WebElement.cs index 805dd0a625e8d..9fd4713dee863 100644 --- a/dotnet/src/webdriver/WebElement.cs +++ b/dotnet/src/webdriver/WebElement.cs @@ -81,7 +81,7 @@ public virtual string TagName if (commandResponse.Value is null) { - throw new WebDriverException($"GetElementTagName command was successful, but response was not an object: {commandResponse.Value}"); + throw new WebDriverException("GetElementTagName command returned a successful result, but contained no data"); } return commandResponse.Value.ToString()!; } @@ -103,7 +103,7 @@ public virtual string Text if (commandResponse.Value is null) { - throw new WebDriverException($"GetElementText command was successful, but response was not an object: {commandResponse.Value}"); + throw new WebDriverException("GetElementText command returned a successful result, but contained no data"); } return commandResponse.Value.ToString()!; From c07436f9154b40b719c0960e4ccc453eb5429bdd Mon Sep 17 00:00:00 2001 From: Michael Render Date: Wed, 5 Feb 2025 21:48:49 -0500 Subject: [PATCH 6/6] Use new `EnsureValueIsNotNull` hlper --- dotnet/src/webdriver/WebElement.cs | 33 +++++++++--------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/dotnet/src/webdriver/WebElement.cs b/dotnet/src/webdriver/WebElement.cs index 9fd4713dee863..ad8e9e720070c 100644 --- a/dotnet/src/webdriver/WebElement.cs +++ b/dotnet/src/webdriver/WebElement.cs @@ -79,10 +79,7 @@ public virtual string TagName Response commandResponse = this.Execute(DriverCommand.GetElementTagName, parameters); - if (commandResponse.Value is null) - { - throw new WebDriverException("GetElementTagName command returned a successful result, but contained no data"); - } + commandResponse.EnsureValueIsNotNull(); return commandResponse.Value.ToString()!; } } @@ -101,11 +98,7 @@ public virtual string Text Response commandResponse = this.Execute(DriverCommand.GetElementText, parameters); - if (commandResponse.Value is null) - { - throw new WebDriverException("GetElementText command returned a successful result, but contained no data"); - } - + commandResponse.EnsureValueIsNotNull(); return commandResponse.Value.ToString()!; } } @@ -248,11 +241,7 @@ public virtual string ComputedAccessibleLabel Response commandResponse = this.Execute(DriverCommand.GetComputedAccessibleLabel, parameters); - if (commandResponse.Value is null) - { - throw new WebDriverException("GetComputedAccessibleLabel command returned a successful result, but contained no data"); - } - + commandResponse.EnsureValueIsNotNull(); return commandResponse.Value.ToString()!; } } @@ -559,11 +548,7 @@ public virtual string GetCssValue(string propertyName) Response commandResponse = this.Execute(DriverCommand.GetElementValueOfCssProperty, parameters); - if (commandResponse.Value is null) - { - throw new WebDriverException("GetElementValueOfCssProperty command returned a successful result, but contained no data"); - } - + commandResponse.EnsureValueIsNotNull(); return commandResponse.Value.ToString()!; } @@ -578,7 +563,9 @@ public virtual Screenshot GetScreenshot() // Get the screenshot as base64. Response screenshotResponse = this.Execute(DriverCommand.ElementScreenshot, parameters); - string base64 = screenshotResponse.Value?.ToString() ?? throw new WebDriverException("ElementScreenshot command returned successfully, but with no response"); + + screenshotResponse.EnsureValueIsNotNull(); + string base64 = screenshotResponse.Value.ToString()!; // ... and convert it. return new Screenshot(base64); @@ -747,8 +734,6 @@ private static string GetAtom(string atomResourceName) return wrappedAtom; } -#nullable restore - private string UploadFile(string localFile) { string base64zip; @@ -767,7 +752,9 @@ private string UploadFile(string localFile) Dictionary parameters = new Dictionary(); parameters.Add("file", base64zip); Response response = this.Execute(DriverCommand.UploadFile, parameters); - return response.Value.ToString(); + + response.EnsureValueIsNotNull(); + return response.Value.ToString()!; } catch (IOException e) {