From 4d4c7cc050e413dfad94de2bf10d69a4dd618ba9 Mon Sep 17 00:00:00 2001 From: sinpru Date: Thu, 13 Nov 2025 11:08:03 +0700 Subject: [PATCH 1/2] Fix CSS selector support for numeric IDs and escape sequences --- .../Controllers/FindElementsController.cs | 56 ++++++++++++++++++- .../Services/ConditionParser.cs | 1 + 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/FlaUI.WebDriver/Controllers/FindElementsController.cs b/src/FlaUI.WebDriver/Controllers/FindElementsController.cs index 528ddb9..4e00666 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,47 @@ private Session GetSession(string sessionId) session.SetLastCommandTimeToNow(); return session; } + + private (string strategy, string value) ParseCssSelector(string cssSelector) + { + if (cssSelector.StartsWith("#")) + { + return ("id", cssSelector.Substring(1)); + } + if (cssSelector.StartsWith(".")) + { + return ("class name", cssSelector.Substring(1)); + } + + 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, @"\*?\[id\s*=\s*""(.+?)""\]", RegexOptions.IgnoreCase); + if (idMatch.Success) + { + return ("id", UnescapeCssValue(idMatch.Groups[1].Value)); + } + + var classMatch = Regex.Match(cssSelector, @"\*?\[class\s*=\s*""(.+?)""\]", RegexOptions.IgnoreCase); + if (classMatch.Success) + { + return ("class name", UnescapeCssValue(classMatch.Groups[1].Value)); + } + + return ("name", cssSelector); + } + + 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": From f83d28a50e4c70172fa516f9fbebd15e25efc16c Mon Sep 17 00:00:00 2001 From: sinpru Date: Sat, 6 Dec 2025 19:02:46 +0700 Subject: [PATCH 2/2] Fix CSS selector regex --- .../Controllers/FindElementsController.cs | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/FlaUI.WebDriver/Controllers/FindElementsController.cs b/src/FlaUI.WebDriver/Controllers/FindElementsController.cs index 4e00666..66212ce 100644 --- a/src/FlaUI.WebDriver/Controllers/FindElementsController.cs +++ b/src/FlaUI.WebDriver/Controllers/FindElementsController.cs @@ -155,34 +155,40 @@ private Session GetSession(string sessionId) private (string strategy, string value) ParseCssSelector(string cssSelector) { - if (cssSelector.StartsWith("#")) - { - return ("id", cssSelector.Substring(1)); - } - if (cssSelector.StartsWith(".")) - { - return ("class name", cssSelector.Substring(1)); - } - - var nameMatch = Regex.Match(cssSelector, @"\*?\[name\s*=\s*""(.+?)""\]", RegexOptions.IgnoreCase); + 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, @"\*?\[id\s*=\s*""(.+?)""\]", RegexOptions.IgnoreCase); + + 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, @"\*?\[class\s*=\s*""(.+?)""\]", RegexOptions.IgnoreCase); + + var classMatch = Regex.Match(cssSelector, @"^\.((?:[\w-]|\\[0-9a-fA-F]{1,6}\s?)+)$"); if (classMatch.Success) { return ("class name", UnescapeCssValue(classMatch.Groups[1].Value)); } - return ("name", cssSelector); + 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)