Serialize Selenium By in C#
Tags: selenium, testing
Published: 2019-05-01 01:38:14

Personally I'm not too happy with the C# bindings to selenium, at some point I'd like to identify good contributions useful to most. One I have used is an inherented By object to be able store locators in a json configuration. 


This object allows for parsing locators directly into usable By object. I've had versions which allow for friendly names and even combining different locators (like chain but in locator string).

The inheritance is critical to providing a friendly name which will print when a standard selenium function fails (because someone thought using access attributes was a good idea).

I've considered creating action objects for the standard element interaction. This actually is not the hard part, handling the differences in how and what to expect from those actions in a generic manner can be. 

```csharp
    public class ElementIdentifier : By
    {
        [JsonProperty]
        [JsonConverter(typeof(StringEnumConverter))]
        public readonly How Type = How.XPath;
        [JsonProperty]
        public readonly string LocatorString;
        [JsonProperty]
        protected readonly string Window;
        [JsonProperty]
        protected readonly string Frame;
        protected By Locator;

        public ElementIdentifier() { }
        private ElementIdentifier(ElementIdentifier locator, string window = null, string frame = null)
        {
            Description = "";
            this.Locator = locator.Locator;
            this.LocatorString = locator.LocatorString;
            this.Type = locator.Type;
            this.Window = window;
            this.Frame = frame;
        }
        public ElementIdentifier(How t, string by)
        {
            Type = t;
            LocatorString = by;
            SetupLocator();
        }
        private ElementIdentifier(ByChained obj)
        {
            Description = "";
            Locator = obj;
            Type = How.Custom;
        }
        public ElementIdentifier Describe(string name)
        {
            if (!string.IsNullOrEmpty(name))
                Description = name;
            return this;
        }

        public ElementIdentifier PageContext(string window = null, string frame = null)
        {
            return new ElementIdentifier(this, window, frame).Describe(Description);
        }
        public ElementIdentifier PageContext(ElementIdentifier rhs)
        {
            return new ElementIdentifier(this, rhs.Window, rhs.Frame).Describe(Description);
        }

        public ElementIdentifier Context(ElementIdentifier rhs)
        {
            if (Type != rhs.Type)
                return new ElementIdentifier(new ByChained(new[] { rhs, this })).Describe(Description).PageContext(rhs);
            if (Type == How.XPath)
                return ElementIdentifier.Union(this, rhs).PageContext(rhs);
            if (Type == How.CssSelector)
                return ElementIdentifier.Union(this, rhs).PageContext(rhs);

            return new ElementIdentifier(new ByChained(new[] { rhs, this })).Describe(Description).PageContext(rhs);
        }

        public static ElementIdentifier Union(ElementIdentifier lhs, ElementIdentifier rhs)
        {
            if (null == lhs)
            {
                return rhs;
            }
            if (null == rhs)
            {
                return lhs;
            }

            if (lhs.Type == How.CssSelector && rhs.Type == How.XPath)
                throw new Exception($"Cannot combine locator {lhs.Type} with {rhs.Type}");
            if (lhs.Type == How.XPath && rhs.Type == How.CssSelector)
                throw new Exception($"Cannot combine locator {lhs.Type} with {rhs.Type}");

            How newType = How.XPath;
            if (lhs.Type == How.CssSelector || rhs.Type == How.CssSelector)
                newType = How.CssSelector;

            string newLocatorString = LocatorText(lhs, newType);
            switch (newType)
            {
                case How.CssSelector:
                    newLocatorString += ", ";
                    break;
                case How.XPath:
                    newLocatorString += " | ";
                    break;
            }

            newLocatorString += LocatorText(rhs, newType);

            return new ElementIdentifier(newType, newLocatorString);
        }

        private static string LocatorText(ElementIdentifier rhs, How newType)
        {
            string newLocatorString = string.Empty;
            switch (rhs.Type)
            {
                case How.ClassName:
                    if (newType == How.XPath)
                        newLocatorString = $"//*[@class='{rhs.LocatorString}']";
                    else
                        newLocatorString = $".{rhs.LocatorString}";
                    break;
                case How.CssSelector:
                    newLocatorString = $"{rhs.LocatorString}";
                    break;
                case How.Id:
                    if (newType == How.XPath)
                        newLocatorString = $"//*[@id='{rhs.LocatorString}']";
                    else
                        newLocatorString = $"#{rhs.LocatorString}";
                    break;
                case How.LinkText:
                    if (newType == How.XPath)
                        newLocatorString = $"//*[text()='{rhs.LocatorString}']";
                    else
                        newLocatorString = $"@value={rhs.LocatorString}";
                    break;
                case How.Name:
                    if (newType == How.XPath)
                        newLocatorString = $"//*[name()='{rhs.LocatorString}']";
                    else
                        newLocatorString = $"@name={rhs.LocatorString}";
                    break;
                case How.PartialLinkText:
                    //TODO: what is the name xpath/css
                    break;
                case How.TagName:
                    //TODO: what is the name xpath/css
                    break;
                case How.XPath:
                    newLocatorString = $"{rhs.LocatorString}";
                    break;
            }
            return newLocatorString;
        }

        //
        // Summary:
        //     Finds the first element matching the criteria.
        //
        // Parameters:
        //   context:
        //     An OpenQA.Selenium.ISearchContext object to use to search for the elements.
        //
        // Returns:
        //     The first matching OpenQA.Selenium.IWebElement on the current context.
        public override IWebElement FindElement(ISearchContext context)
        {
            return Locator.FindElement(context);
        }
        //
        // Summary:
        //     Finds all elements matching the criteria.
        //
        // Parameters:
        //   context:
        //     An OpenQA.Selenium.ISearchContext object to use to search for the elements.
        //
        // Returns:
        //     A System.Collections.ObjectModel.ReadOnlyCollection`1 of all OpenQA.Selenium.IWebElement
        //     matching the current criteria, or an empty list if nothing matches.
        public override ReadOnlyCollection<IWebElement> FindElements(ISearchContext context)
        {
            return Locator.FindElements(context);
        }

        [System.Runtime.Serialization.OnDeserialized]
        internal void OnDeserializedMethod(System.Runtime.Serialization.StreamingContext context)
        {
            if (string.IsNullOrEmpty(LocatorString)) return;
            SetupLocator();
        }

        private void SetupLocator()
        {
            switch (Type)
            {
                case How.ClassName:
                    Locator = ClassName(LocatorString);
                    break;
                case How.CssSelector:
                    Locator = CssSelector(LocatorString);
                    break;
                case How.Id:
                    Locator = Id(LocatorString);
                    break;
                case How.LinkText:
                    Locator = LinkText(LocatorString);
                    break;
                case How.Name:
                    Locator = Name(LocatorString);
                    break;
                case How.PartialLinkText:
                    Locator = PartialLinkText(LocatorString);
                    break;
                case How.TagName:
                    Locator = TagName(LocatorString);
                    break;
                case How.XPath:
                    Locator = XPath(LocatorString);
                    break;
            }

            Description = Locator?.ToString();
        }
    }
```

Now this isn't the version I use today (which is much more stripped down), and this one relies on support function I haven't included. If there is enough interest I should be able to put together some examples.