From 81ad12059fa3ded280366535c1db77bce5854ef2 Mon Sep 17 00:00:00 2001 From: sinpru Date: Thu, 13 Nov 2025 11:08:03 +0700 Subject: [PATCH] Fix CSS selector support for numeric IDs and escape sequences --- .../Services/ConditionParserTests.cs | 79 ++++++++++++++++--- .../Services/ConditionParser.cs | 31 +++++--- 2 files changed, 89 insertions(+), 21 deletions(-) diff --git a/src/FlaUI.WebDriver.UnitTests/Services/ConditionParserTests.cs b/src/FlaUI.WebDriver.UnitTests/Services/ConditionParserTests.cs index f552344..f3cce19 100644 --- a/src/FlaUI.WebDriver.UnitTests/Services/ConditionParserTests.cs +++ b/src/FlaUI.WebDriver.UnitTests/Services/ConditionParserTests.cs @@ -1,22 +1,79 @@ -using FlaUI.WebDriver.Services; +using FlaUI.Core.Conditions; +using FlaUI.UIA3; +using FlaUI.WebDriver.Services; using NUnit.Framework; namespace FlaUI.WebDriver.UnitTests.Services { public class ConditionParserTests { - [TestCase("[name=\"2\"]")] - [TestCase("*[name=\"2\"]")] - [TestCase("*[name = \"2\"]")] - public void ParseCondition_ByCssAttributeName_ReturnsCondition(string selector) + private ConditionParser _conditionParser; + private ConditionFactory _conditionFactory; + + [SetUp] + public void Setup() + { + _conditionParser = new ConditionParser(); + var automation = new UIA3Automation(); + _conditionFactory = automation.ConditionFactory; + } + + [Test] + public void ParseCondition_CssSelectorWithNumericIdUsingUnicodeEscape_ReturnsAutomationIdCondition() + { + var cssSelector = @"#\34 b090d48-e3a5-4eb4-bd37-4bd62dfa6e5b"; + + var condition = _conditionParser.ParseCondition(_conditionFactory, "css selector", cssSelector); + + Assert.That(condition.Value, Is.EqualTo("4b090d48-e3a5-4eb4-bd37-4bd62dfa6e5b")); + } + + [Test] + public void ParseCondition_CssSelectorWithSimpleNumericId_ReturnsAutomationIdCondition() + { + var cssSelector = @"#\31 "; + + var condition = _conditionParser.ParseCondition(_conditionFactory, "css selector", cssSelector); + + Assert.That(condition.Value, Is.EqualTo("1")); + } + + [Test] + public void ParseCondition_CssSelectorWithEscapedSpecialChars_ReturnsNameCondition() { - var parser = new ConditionParser(); - var uia3 = new UIA3.UIA3Automation(); + var cssSelector = @"*[name=""ListBox\ Item\ \#1""]"; - var result = parser.ParseCondition(uia3.ConditionFactory, "css selector", selector); + var condition = _conditionParser.ParseCondition(_conditionFactory, "css selector", cssSelector); + + Assert.That(condition.Value, Is.EqualTo("ListBox Item #1")); + } + + [Test] + public void ParseCondition_CssSelectorCompoundSelector_ThrowsUnsupportedOperation() + { + var cssSelector = "#foo.bar"; + + Assert.Throws(() => + _conditionParser.ParseCondition(_conditionFactory, "css selector", cssSelector)); + } + + [Test] + public void ParseCondition_PlainIdStrategy_ReturnsAutomationIdCondition() + { + var id = "TextBox"; + + var condition = _conditionParser.ParseCondition(_conditionFactory, "id", id); + + Assert.That(condition.Value, Is.EqualTo("TextBox")); + } + + [Test] + public void ParseCondition_CssSelectorCompoundAttributeSelector_ThrowsUnsupportedOperation() + { + var cssSelector = "[name=\"test\"][class=\"test2\"]"; - Assert.That(result.Property, Is.EqualTo(uia3.PropertyLibrary.Element.Name)); - Assert.That(result.Value, Is.EqualTo("2")); + Assert.Throws(() => + _conditionParser.ParseCondition(_conditionFactory, "css selector", cssSelector)); } } -} +} \ No newline at end of file diff --git a/src/FlaUI.WebDriver/Services/ConditionParser.cs b/src/FlaUI.WebDriver/Services/ConditionParser.cs index 16c6d9c..3b5bf5b 100644 --- a/src/FlaUI.WebDriver/Services/ConditionParser.cs +++ b/src/FlaUI.WebDriver/Services/ConditionParser.cs @@ -9,41 +9,44 @@ public class ConditionParser : IConditionParser /// /// Based on https://www.w3.org/TR/CSS21/grammar.html (see also https://www.w3.org/TR/CSS22/grammar.html) /// Limitations: - /// - Unicode escape characters are not supported. /// - Multiple selectors are not supported. /// - private static Regex SimpleCssIdSelectorRegex = new Regex(@"^#(?(?[_a-z0-9-]|[\240-\377]|(?\\[^\r\n\f0-9a-f]))+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static Regex SimpleCssIdSelectorRegex = new Regex(@"^#(?(?[_a-z0-9-]|[\240-\377]|(?\\[^\r\n\f0-9a-f])|(?\\[0-9a-fA-F]{1,6}\s?))+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// /// Based on https://www.w3.org/TR/CSS21/grammar.html (see also https://www.w3.org/TR/CSS22/grammar.html) /// Limitations: - /// - Unicode escape characters are not supported. /// - Multiple selectors are not supported. /// - private static Regex SimpleCssClassSelectorRegex = new Regex(@"^\.(?-?(?[_a-z]|[\240-\377])(?[_a-z0-9-]|[\240-\377]|(?\\[^\r\n\f0-9a-f]))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static Regex SimpleCssClassSelectorRegex = new Regex(@"^\.(?-?(?[_a-z]|[\240-\377])(?[_a-z0-9-]|[\240-\377]|(?\\[^\r\n\f0-9a-f])|(?\\[0-9a-fA-F]{1,6}\s?))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// /// Based on https://www.w3.org/TR/CSS21/grammar.html (see also https://www.w3.org/TR/CSS22/grammar.html) /// Limitations: - /// - Unicode escape characters or escape characters in the attribute name are not supported. + /// - Escape characters in the attribute name are not supported. /// - Multiple selectors are not supported. /// - Attribute presence selector (e.g. `[name]`) not supported. /// - Attribute equals attribute (e.g. `[name=value]`) not supported. /// - ~= or |= not supported. /// - private static Regex SimpleCssAttributeSelectorRegex = new Regex(@"^\*?\[\s*(?-?(?[_a-z]|[\240-\377])(?[_a-z0-9-]|[\240-\377])*)\s*=\s*(?(?""(?([^\n\r\f\\""]|(?\\[^\r\n\f0-9a-f]))*)"")|(?'(?([^\n\r\f\\']|(?\\[^\r\n\f0-9a-f]))*)'))\s*\]$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static Regex SimpleCssAttributeSelectorRegex = new Regex(@"^\*?\[\s*(?-?(?[_a-z]|[\240-\377])(?[_a-z0-9-]|[\240-\377])*)\s*=\s*(?(?""(?([^\n\r\f\\""]|(?\\[^\r\n\f0-9a-f])|(?\\[0-9a-fA-F]{1,6}\s?))*)"")|(?'(?([^\n\r\f\\']|(?\\[^\r\n\f0-9a-f])|(?\\[0-9a-fA-F]{1,6}\s?))*)'))\s*\]$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// /// Based on https://www.w3.org/TR/CSS21/grammar.html (see also https://www.w3.org/TR/CSS22/grammar.html) - /// Limitations: - /// - Unicode escape characters are not supported. + /// Matches simple escape characters (e.g., \#) /// private static Regex SimpleCssEscapeCharacterRegex = new Regex(@"\\[^\r\n\f0-9a-f]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + /// + /// Matches CSS unicode escape sequences (e.g., \34 or \000034 followed by optional space) + /// + private static Regex CssUnicodeEscapeRegex = new Regex(@"\\([0-9a-fA-F]{1,6})\s?", RegexOptions.Compiled); + public PropertyCondition ParseCondition(ConditionFactory conditionFactory, string @using, string value) { switch (@using) { + case "id": case "accessibility id": return conditionFactory.ByAutomationId(value); case "name": @@ -86,8 +89,16 @@ public PropertyCondition ParseCondition(ConditionFactory conditionFactory, strin private static string ReplaceCssEscapedCharacters(string value) { - return SimpleCssEscapeCharacterRegex.Replace(value, match => match.Value.Substring(1)); - } + var result = CssUnicodeEscapeRegex.Replace(value, m => + { + var hexValue = m.Groups[1].Value; + var decodedChar = ((char)Convert.ToInt32(hexValue, 16)).ToString(); + return decodedChar; + }); + result = SimpleCssEscapeCharacterRegex.Replace(result, match => match.Value.Substring(1)); + + return result; + } } }