diff --git a/src/FlaUI.WebDriver/Controllers/FindElementsController.cs b/src/FlaUI.WebDriver/Controllers/FindElementsController.cs index 528ddb9..66212ce 100644 --- a/src/FlaUI.WebDriver/Controllers/FindElementsController.cs +++ b/src/FlaUI.WebDriver/Controllers/FindElementsController.cs @@ -60,6 +60,12 @@ private async Task FindElementFrom(Func startNo { element = await Wait.Until(() => startNode().FindFirstByXPath(findElementRequest.Value), element => element != null, session.ImplicitWaitTimeout); } + else if (findElementRequest.Using == "css selector") + { + var (strategy, value) = ParseCssSelector(findElementRequest.Value); + var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, strategy, value); + element = await Wait.Until(() => startNode().FindFirstDescendant(condition), element => element != null, session.ImplicitWaitTimeout); + } else { var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, findElementRequest.Using, findElementRequest.Value); @@ -71,7 +77,7 @@ private async Task FindElementFrom(Func startNo return NoSuchElement(findElementRequest); } - var knownElement = session.GetOrAddKnownElement(element); + var knownElement = session.GetOrAddKnownElement(element); return await Task.FromResult(WebDriverResult.Success(new FindElementResponse { ElementReference = knownElement.ElementReference, @@ -85,6 +91,12 @@ private async Task FindElementsFrom(Func startN { elements = await Wait.Until(() => startNode().FindAllByXPath(findElementRequest.Value), elements => elements.Length > 0, session.ImplicitWaitTimeout); } + else if (findElementRequest.Using == "css selector") + { + var (strategy, value) = ParseCssSelector(findElementRequest.Value); + var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, strategy, value); + elements = await Wait.Until(() => startNode().FindAllDescendants(condition), elements => elements.Length > 0, session.ImplicitWaitTimeout); + } else { var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, findElementRequest.Using, findElementRequest.Value); @@ -140,5 +152,53 @@ private Session GetSession(string sessionId) session.SetLastCommandTimeToNow(); return session; } + + private (string strategy, string value) ParseCssSelector(string cssSelector) + { + var nameMatch = Regex.Match(cssSelector, @"^\*?\[name\s*=\s*""(.+?)""\]$", RegexOptions.IgnoreCase); + if (nameMatch.Success) + { + return ("name", UnescapeCssValue(nameMatch.Groups[1].Value)); + } + + var idMatch = Regex.Match(cssSelector, @"^#((?:[\w-]|\\[0-9a-fA-F]{1,6}\s?)+)$"); + if (idMatch.Success) + { + return ("id", UnescapeCssValue(idMatch.Groups[1].Value)); + } + + var classMatch = Regex.Match(cssSelector, @"^\.((?:[\w-]|\\[0-9a-fA-F]{1,6}\s?)+)$"); + if (classMatch.Success) + { + return ("class name", UnescapeCssValue(classMatch.Groups[1].Value)); + } + + var idAttrMatch = Regex.Match(cssSelector, @"^\*?\[id\s*=\s*""(.+?)""\]$", RegexOptions.IgnoreCase); + if (idAttrMatch.Success) + { + return ("id", UnescapeCssValue(idAttrMatch.Groups[1].Value)); + } + + var classAttrMatch = Regex.Match(cssSelector, @"^\*?\[class\s*=\s*""(.+?)""\]$", RegexOptions.IgnoreCase); + if (classAttrMatch.Success) + { + return ("class name", UnescapeCssValue(classAttrMatch.Groups[1].Value)); + } + + throw WebDriverResponseException.UnsupportedOperation( + $"CSS selector '{cssSelector}' is not supported. Only simple selectors are allowed: " + + "#id, .className, [name=\"value\"], [id=\"value\"], [class=\"value\"]" + ); + } + + private string UnescapeCssValue(string cssValue) + { + var result = Regex.Replace(cssValue, @"\\([0-9a-fA-F]{1,6})\s?", m => + ((char)Convert.ToInt32(m.Groups[1].Value, 16)).ToString()); + + result = Regex.Replace(result, @"\\(.)", "$1"); + + return result; + } } } diff --git a/src/FlaUI.WebDriver/Services/ConditionParser.cs b/src/FlaUI.WebDriver/Services/ConditionParser.cs index 16c6d9c..22c4dd4 100644 --- a/src/FlaUI.WebDriver/Services/ConditionParser.cs +++ b/src/FlaUI.WebDriver/Services/ConditionParser.cs @@ -44,6 +44,7 @@ public PropertyCondition ParseCondition(ConditionFactory conditionFactory, strin { switch (@using) { + case "id": case "accessibility id": return conditionFactory.ByAutomationId(value); case "name":