Skip to content

Commit bbc21dd

Browse files
committed
Refactored CSS selector parsing inside ConditionParser, added tests to check the current issue
1 parent f83d28a commit bbc21dd

File tree

3 files changed

+79
-81
lines changed

3 files changed

+79
-81
lines changed
Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,70 @@
1-
using FlaUI.WebDriver.Services;
1+
using FlaUI.Core.Conditions;
2+
using FlaUI.UIA3;
3+
using FlaUI.WebDriver.Services;
24
using NUnit.Framework;
35

46
namespace FlaUI.WebDriver.UnitTests.Services
57
{
68
public class ConditionParserTests
79
{
8-
[TestCase("[name=\"2\"]")]
9-
[TestCase("*[name=\"2\"]")]
10-
[TestCase("*[name = \"2\"]")]
11-
public void ParseCondition_ByCssAttributeName_ReturnsCondition(string selector)
10+
private ConditionParser _conditionParser;
11+
private ConditionFactory _conditionFactory;
12+
13+
[SetUp]
14+
public void Setup()
15+
{
16+
_conditionParser = new ConditionParser();
17+
var automation = new UIA3Automation();
18+
_conditionFactory = automation.ConditionFactory;
19+
}
20+
21+
[Test]
22+
public void ParseCondition_CssSelectorWithNumericIdUsingUnicodeEscape_ReturnsAutomationIdCondition()
23+
{
24+
var cssSelector = @"#\34 b090d48-e3a5-4eb4-bd37-4bd62dfa6e5b";
25+
26+
var condition = _conditionParser.ParseCondition(_conditionFactory, "css selector", cssSelector);
27+
28+
Assert.That(condition.Value, Is.EqualTo("4b090d48-e3a5-4eb4-bd37-4bd62dfa6e5b"));
29+
}
30+
31+
[Test]
32+
public void ParseCondition_CssSelectorWithSimpleNumericId_ReturnsAutomationIdCondition()
33+
{
34+
var cssSelector = @"#\31 ";
35+
36+
var condition = _conditionParser.ParseCondition(_conditionFactory, "css selector", cssSelector);
37+
38+
Assert.That(condition.Value, Is.EqualTo("1"));
39+
}
40+
41+
[Test]
42+
public void ParseCondition_CssSelectorWithEscapedSpecialChars_ReturnsNameCondition()
43+
{
44+
var cssSelector = @"*[name=""ListBox\ Item\ \#1""]";
45+
46+
var condition = _conditionParser.ParseCondition(_conditionFactory, "css selector", cssSelector);
47+
48+
Assert.That(condition.Value, Is.EqualTo("ListBox Item #1"));
49+
}
50+
51+
[Test]
52+
public void ParseCondition_CssSelectorCompoundSelector_ThrowsUnsupportedOperation()
53+
{
54+
var cssSelector = "#foo.bar";
55+
56+
Assert.Throws<WebDriverResponseException>(() =>
57+
_conditionParser.ParseCondition(_conditionFactory, "css selector", cssSelector));
58+
}
59+
60+
[Test]
61+
public void ParseCondition_PlainIdStrategy_ReturnsAutomationIdCondition()
1262
{
13-
var parser = new ConditionParser();
14-
var uia3 = new UIA3.UIA3Automation();
63+
var id = "TextBox";
1564

16-
var result = parser.ParseCondition(uia3.ConditionFactory, "css selector", selector);
65+
var condition = _conditionParser.ParseCondition(_conditionFactory, "id", id);
1766

18-
Assert.That(result.Property, Is.EqualTo(uia3.PropertyLibrary.Element.Name));
19-
Assert.That(result.Value, Is.EqualTo("2"));
67+
Assert.That(condition.Value, Is.EqualTo("TextBox"));
2068
}
2169
}
22-
}
70+
}

src/FlaUI.WebDriver/Controllers/FindElementsController.cs

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,6 @@ private async Task<ActionResult> FindElementFrom(Func<AutomationElement> startNo
6060
{
6161
element = await Wait.Until(() => startNode().FindFirstByXPath(findElementRequest.Value), element => element != null, session.ImplicitWaitTimeout);
6262
}
63-
else if (findElementRequest.Using == "css selector")
64-
{
65-
var (strategy, value) = ParseCssSelector(findElementRequest.Value);
66-
var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, strategy, value);
67-
element = await Wait.Until(() => startNode().FindFirstDescendant(condition), element => element != null, session.ImplicitWaitTimeout);
68-
}
6963
else
7064
{
7165
var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, findElementRequest.Using, findElementRequest.Value);
@@ -77,7 +71,7 @@ private async Task<ActionResult> FindElementFrom(Func<AutomationElement> startNo
7771
return NoSuchElement(findElementRequest);
7872
}
7973

80-
var knownElement = session.GetOrAddKnownElement(element);
74+
var knownElement = session.GetOrAddKnownElement(element);
8175
return await Task.FromResult(WebDriverResult.Success(new FindElementResponse
8276
{
8377
ElementReference = knownElement.ElementReference,
@@ -91,12 +85,6 @@ private async Task<ActionResult> FindElementsFrom(Func<AutomationElement> startN
9185
{
9286
elements = await Wait.Until(() => startNode().FindAllByXPath(findElementRequest.Value), elements => elements.Length > 0, session.ImplicitWaitTimeout);
9387
}
94-
else if (findElementRequest.Using == "css selector")
95-
{
96-
var (strategy, value) = ParseCssSelector(findElementRequest.Value);
97-
var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, strategy, value);
98-
elements = await Wait.Until(() => startNode().FindAllDescendants(condition), elements => elements.Length > 0, session.ImplicitWaitTimeout);
99-
}
10088
else
10189
{
10290
var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, findElementRequest.Using, findElementRequest.Value);
@@ -152,53 +140,5 @@ private Session GetSession(string sessionId)
152140
session.SetLastCommandTimeToNow();
153141
return session;
154142
}
155-
156-
private (string strategy, string value) ParseCssSelector(string cssSelector)
157-
{
158-
var nameMatch = Regex.Match(cssSelector, @"^\*?\[name\s*=\s*""(.+?)""\]$", RegexOptions.IgnoreCase);
159-
if (nameMatch.Success)
160-
{
161-
return ("name", UnescapeCssValue(nameMatch.Groups[1].Value));
162-
}
163-
164-
var idMatch = Regex.Match(cssSelector, @"^#((?:[\w-]|\\[0-9a-fA-F]{1,6}\s?)+)$");
165-
if (idMatch.Success)
166-
{
167-
return ("id", UnescapeCssValue(idMatch.Groups[1].Value));
168-
}
169-
170-
var classMatch = Regex.Match(cssSelector, @"^\.((?:[\w-]|\\[0-9a-fA-F]{1,6}\s?)+)$");
171-
if (classMatch.Success)
172-
{
173-
return ("class name", UnescapeCssValue(classMatch.Groups[1].Value));
174-
}
175-
176-
var idAttrMatch = Regex.Match(cssSelector, @"^\*?\[id\s*=\s*""(.+?)""\]$", RegexOptions.IgnoreCase);
177-
if (idAttrMatch.Success)
178-
{
179-
return ("id", UnescapeCssValue(idAttrMatch.Groups[1].Value));
180-
}
181-
182-
var classAttrMatch = Regex.Match(cssSelector, @"^\*?\[class\s*=\s*""(.+?)""\]$", RegexOptions.IgnoreCase);
183-
if (classAttrMatch.Success)
184-
{
185-
return ("class name", UnescapeCssValue(classAttrMatch.Groups[1].Value));
186-
}
187-
188-
throw WebDriverResponseException.UnsupportedOperation(
189-
$"CSS selector '{cssSelector}' is not supported. Only simple selectors are allowed: " +
190-
"#id, .className, [name=\"value\"], [id=\"value\"], [class=\"value\"]"
191-
);
192-
}
193-
194-
private string UnescapeCssValue(string cssValue)
195-
{
196-
var result = Regex.Replace(cssValue, @"\\([0-9a-fA-F]{1,6})\s?", m =>
197-
((char)Convert.ToInt32(m.Groups[1].Value, 16)).ToString());
198-
199-
result = Regex.Replace(result, @"\\(.)", "$1");
200-
201-
return result;
202-
}
203143
}
204144
}

src/FlaUI.WebDriver/Services/ConditionParser.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,16 @@ public class ConditionParser : IConditionParser
99
/// <summary>
1010
/// Based on https://www.w3.org/TR/CSS21/grammar.html (see also https://www.w3.org/TR/CSS22/grammar.html)
1111
/// Limitations:
12-
/// - Unicode escape characters are not supported.
1312
/// - Multiple selectors are not supported.
1413
/// </summary>
15-
private static Regex SimpleCssIdSelectorRegex = new Regex(@"^#(?<name>(?<nmchar>[_a-z0-9-]|[\240-\377]|(?<escape>\\[^\r\n\f0-9a-f]))+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
14+
private static Regex SimpleCssIdSelectorRegex = new Regex(@"^#(?<name>(?<nmchar>[_a-z0-9-]|[\240-\377]|(?<escape>\\[^\r\n\f0-9a-f])|(?<unicode>\\[0-9a-fA-F]{1,6}\s?))+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
1615

1716
/// <summary>
1817
/// Based on https://www.w3.org/TR/CSS21/grammar.html (see also https://www.w3.org/TR/CSS22/grammar.html)
1918
/// Limitations:
20-
/// - Unicode escape characters are not supported.
2119
/// - Multiple selectors are not supported.
2220
/// </summary>
23-
private static Regex SimpleCssClassSelectorRegex = new Regex(@"^\.(?<ident>-?(?<nmstart>[_a-z]|[\240-\377])(?<nmchar>[_a-z0-9-]|[\240-\377]|(?<escape>\\[^\r\n\f0-9a-f]))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
21+
private static Regex SimpleCssClassSelectorRegex = new Regex(@"^\.(?<ident>-?(?<nmstart>[_a-z]|[\240-\377])(?<nmchar>[_a-z0-9-]|[\240-\377]|(?<escape>\\[^\r\n\f0-9a-f])|(?<unicode>\\[0-9a-fA-F]{1,6}\s?))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
2422

2523
/// <summary>
2624
/// Based on https://www.w3.org/TR/CSS21/grammar.html (see also https://www.w3.org/TR/CSS22/grammar.html)
@@ -31,15 +29,19 @@ public class ConditionParser : IConditionParser
3129
/// - Attribute equals attribute (e.g. `[name=value]`) not supported.
3230
/// - ~= or |= not supported.
3331
/// </summary>
34-
private static Regex SimpleCssAttributeSelectorRegex = new Regex(@"^\*?\[\s*(?<ident>-?(?<nmstart>[_a-z]|[\240-\377])(?<nmchar>[_a-z0-9-]|[\240-\377])*)\s*=\s*(?<string>(?<string1>""(?<string1value>([^\n\r\f\\""]|(?<escape>\\[^\r\n\f0-9a-f]))*)"")|(?<string2>'(?<string2value>([^\n\r\f\\']|(?<escape>\\[^\r\n\f0-9a-f]))*)'))\s*\]$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
32+
private static Regex SimpleCssAttributeSelectorRegex = new Regex(@"^\*?\[\s*(?<ident>-?(?<nmstart>[_a-z]|[\240-\377])(?<nmchar>[_a-z0-9-]|[\240-\377])*)\s*=\s*(?<string>(?<string1>""(?<string1value>([^\n\r\f\\""]|(?<escape>\\[^\r\n\f0-9a-f])|(?<unicode>\\[0-9a-fA-F]{1,6}\s?))*)"")|(?<string2>'(?<string2value>([^\n\r\f\\']|(?<escape>\\[^\r\n\f0-9a-f])|(?<unicode>\\[0-9a-fA-F]{1,6}\s?))*)'))\s*\]$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
3533

3634
/// <summary>
3735
/// Based on https://www.w3.org/TR/CSS21/grammar.html (see also https://www.w3.org/TR/CSS22/grammar.html)
38-
/// Limitations:
39-
/// - Unicode escape characters are not supported.
36+
/// Matches simple escape characters (e.g., \#)
4037
/// </summary>
4138
private static Regex SimpleCssEscapeCharacterRegex = new Regex(@"\\[^\r\n\f0-9a-f]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
4239

40+
/// <summary>
41+
/// Matches CSS unicode escape sequences (e.g., \34 or \000034 followed by optional space)
42+
/// </summary>
43+
private static Regex CssUnicodeEscapeRegex = new Regex(@"\\([0-9a-fA-F]{1,6})\s?", RegexOptions.Compiled);
44+
4345
public PropertyCondition ParseCondition(ConditionFactory conditionFactory, string @using, string value)
4446
{
4547
switch (@using)
@@ -87,8 +89,16 @@ public PropertyCondition ParseCondition(ConditionFactory conditionFactory, strin
8789

8890
private static string ReplaceCssEscapedCharacters(string value)
8991
{
90-
return SimpleCssEscapeCharacterRegex.Replace(value, match => match.Value.Substring(1));
91-
}
92+
var result = CssUnicodeEscapeRegex.Replace(value, m =>
93+
{
94+
var hexValue = m.Groups[1].Value;
95+
var decodedChar = ((char)Convert.ToInt32(hexValue, 16)).ToString();
96+
return decodedChar;
97+
});
9298

99+
result = SimpleCssEscapeCharacterRegex.Replace(result, match => match.Value.Substring(1));
100+
101+
return result;
102+
}
93103
}
94104
}

0 commit comments

Comments
 (0)