Skip to content

Commit

Permalink
Add unit test for condition parsing
Browse files Browse the repository at this point in the history
Add a unit test project and add a few small unit tests related to #99,
which prove that this is not an issue in FlaUI.WebDriver.
  • Loading branch information
aristotelos committed Oct 4, 2024
1 parent 8c02962 commit e5d543b
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 88 deletions.
24 changes: 24 additions & 0 deletions src/FlaUI.WebDriver.UnitTests/FlaUI.WebDriver.UnitTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>

<IsPackable>false</IsPackable>
<LangVersion>preview</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FlaUI.WebDriver\FlaUI.WebDriver.csproj" />
</ItemGroup>

</Project>
22 changes: 22 additions & 0 deletions src/FlaUI.WebDriver.UnitTests/Services/ConditionParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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)
{
var parser = new ConditionParser();
var uia3 = new UIA3.UIA3Automation();

var result = parser.ParseCondition(uia3.ConditionFactory, "css selector", selector);

Assert.That(result.Property, Is.EqualTo(uia3.PropertyLibrary.Element.Name));
Assert.That(result.Value, Is.EqualTo("2"));
}
}
}
23 changes: 23 additions & 0 deletions src/FlaUI.WebDriver.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
..\README.md = ..\README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlaUI.WebDriver.UnitTests", "FlaUI.WebDriver.UnitTests\FlaUI.WebDriver.UnitTests.csproj", "{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -93,13 +95,34 @@ Global
{23F0E331-C5AE-4D3D-B4E2-534D52E65CA0}.Release|x64.Build.0 = Release|Any CPU
{23F0E331-C5AE-4D3D-B4E2-534D52E65CA0}.Release|x86.ActiveCfg = Release|Any CPU
{23F0E331-C5AE-4D3D-B4E2-534D52E65CA0}.Release|x86.Build.0 = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|ARM.ActiveCfg = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|ARM.Build.0 = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|ARM64.Build.0 = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|x64.ActiveCfg = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|x64.Build.0 = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|x86.ActiveCfg = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Debug|x86.Build.0 = Debug|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|Any CPU.Build.0 = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|ARM.ActiveCfg = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|ARM.Build.0 = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|ARM64.ActiveCfg = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|ARM64.Build.0 = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|x64.ActiveCfg = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|x64.Build.0 = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|x86.ActiveCfg = Release|Any CPU
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5315D9CF-DDA4-49AE-BA92-AB5814E61901} = {3DFE78D4-89EB-4CEE-A5D1-F5FDDED10959}
{23F0E331-C5AE-4D3D-B4E2-534D52E65CA0} = {00BCF82A-388A-4DC9-A1E2-6D6D983BAEE3}
{22701FF4-ADE3-4C05-9E5E-0616A5ED26F3} = {3DFE78D4-89EB-4CEE-A5D1-F5FDDED10959}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F2B64231-45B2-4129-960A-9F26AFFD16AE}
Expand Down
96 changes: 8 additions & 88 deletions src/FlaUI.WebDriver/Controllers/FindElementsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using FlaUI.Core.Definitions;
using FlaUI.Core.AutomationElements;
using System.Text.RegularExpressions;
using FlaUI.WebDriver.Services;

namespace FlaUI.WebDriver.Controllers
{
Expand All @@ -13,11 +14,13 @@ public class FindElementsController : ControllerBase
{
private readonly ILogger<FindElementsController> _logger;
private readonly ISessionRepository _sessionRepository;
private readonly IConditionParser _conditionParser;

public FindElementsController(ILogger<FindElementsController> logger, ISessionRepository sessionRepository)
public FindElementsController(ILogger<FindElementsController> logger, ISessionRepository sessionRepository, IConditionParser conditionParser)
{
_logger = logger;
_sessionRepository = sessionRepository;
_conditionParser = conditionParser;
}

[HttpPost("element")]
Expand Down Expand Up @@ -50,7 +53,7 @@ public async Task<ActionResult> FindElementsFromElement([FromRoute] string sessi
return await FindElementsFrom(() => element, findElementRequest, session);
}

private static async Task<ActionResult> FindElementFrom(Func<AutomationElement> startNode, FindElementRequest findElementRequest, Session session)
private async Task<ActionResult> FindElementFrom(Func<AutomationElement> startNode, FindElementRequest findElementRequest, Session session)
{
AutomationElement? element;
if (findElementRequest.Using == "xpath")
Expand All @@ -59,7 +62,7 @@ private static async Task<ActionResult> FindElementFrom(Func<AutomationElement>
}
else
{
var condition = GetCondition(session.Automation.ConditionFactory, findElementRequest.Using, findElementRequest.Value);
var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, findElementRequest.Using, findElementRequest.Value);
element = await Wait.Until(() => startNode().FindFirstDescendant(condition), element => element != null, session.ImplicitWaitTimeout);
}

Expand All @@ -75,7 +78,7 @@ private static async Task<ActionResult> FindElementFrom(Func<AutomationElement>
}));
}

private static async Task<ActionResult> FindElementsFrom(Func<AutomationElement> startNode, FindElementRequest findElementRequest, Session session)
private async Task<ActionResult> FindElementsFrom(Func<AutomationElement> startNode, FindElementRequest findElementRequest, Session session)
{
AutomationElement[] elements;
if (findElementRequest.Using == "xpath")
Expand All @@ -84,7 +87,7 @@ private static async Task<ActionResult> FindElementsFrom(Func<AutomationElement>
}
else
{
var condition = GetCondition(session.Automation.ConditionFactory, findElementRequest.Using, findElementRequest.Value);
var condition = _conditionParser.ParseCondition(session.Automation.ConditionFactory, findElementRequest.Using, findElementRequest.Value);
elements = await Wait.Until(() => startNode().FindAllDescendants(condition), elements => elements.Length > 0, session.ImplicitWaitTimeout);
}

Expand All @@ -98,89 +101,6 @@ private static async Task<ActionResult> FindElementsFrom(Func<AutomationElement>
));
}

/// <summary>
/// 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.
/// </summary>
private static Regex SimpleCssIdSelectorRegex = new Regex(@"^#(?<name>(?<nmchar>[_a-z0-9-]|[\240-\377]|(?<escape>\\[^\r\n\f0-9a-f]))+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

/// <summary>
/// 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.
/// </summary>
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);

/// <summary>
/// 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.
/// - 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.
/// </summary>
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);

/// <summary>
/// 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.
/// </summary>
private static Regex SimpleCssEscapeCharacterRegex = new Regex(@"\\[^\r\n\f0-9a-f]", RegexOptions.Compiled | RegexOptions.IgnoreCase);

private static PropertyCondition GetCondition(ConditionFactory conditionFactory, string @using, string value)
{
switch (@using)
{
case "accessibility id":
return conditionFactory.ByAutomationId(value);
case "name":
return conditionFactory.ByName(value);
case "class name":
return conditionFactory.ByClassName(value);
case "link text":
return conditionFactory.ByText(value);
case "partial link text":
return conditionFactory.ByText(value, PropertyConditionFlags.MatchSubstring);
case "tag name":
return conditionFactory.ByControlType(Enum.Parse<ControlType>(value));
case "css selector":
var cssIdSelectorMatch = SimpleCssIdSelectorRegex.Match(value);
if (cssIdSelectorMatch.Success)
{
return conditionFactory.ByAutomationId(ReplaceCssEscapedCharacters(value.Substring(1)));
}
var cssClassSelectorMatch = SimpleCssClassSelectorRegex.Match(value);
if (cssClassSelectorMatch.Success)
{
return conditionFactory.ByClassName(ReplaceCssEscapedCharacters(value.Substring(1)));
}
var cssAttributeSelectorMatch = SimpleCssAttributeSelectorRegex.Match(value);
if (cssAttributeSelectorMatch.Success)
{
var attributeValue = ReplaceCssEscapedCharacters(cssAttributeSelectorMatch.Groups["string1value"].Success ?
cssAttributeSelectorMatch.Groups["string1value"].Value :
cssAttributeSelectorMatch.Groups["string2value"].Value);
if (cssAttributeSelectorMatch.Groups["ident"].Value == "name")
{
return conditionFactory.ByName(attributeValue);
}
}
throw WebDriverResponseException.UnsupportedOperation($"Selector strategy 'css selector' with value '{value}' is not supported");
default:
throw WebDriverResponseException.UnsupportedOperation($"Selector strategy '{@using}' is not supported");
}
}

private static string ReplaceCssEscapedCharacters(string value)
{
return SimpleCssEscapeCharacterRegex.Replace(value, match => match.Value.Substring(1));
}

private static ActionResult NoSuchElement(FindElementRequest findElementRequest)
{
return WebDriverResult.NotFound(new ErrorResponse()
Expand Down
1 change: 1 addition & 0 deletions src/FlaUI.WebDriver/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
builder.Services.AddSingleton<ISessionRepository, SessionRepository>();
builder.Services.AddScoped<IActionsDispatcher, ActionsDispatcher>();
builder.Services.AddScoped<IWindowsExtensionService, WindowsExtensionService>();
builder.Services.AddScoped<IConditionParser, ConditionParser>();

builder.Services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
builder.Services.AddControllers(options =>
Expand Down
93 changes: 93 additions & 0 deletions src/FlaUI.WebDriver/Services/ConditionParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using FlaUI.Core.Conditions;
using FlaUI.Core.Definitions;
using System.Text.RegularExpressions;

namespace FlaUI.WebDriver.Services
{
public class ConditionParser : IConditionParser
{
/// <summary>
/// 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.
/// </summary>
private static Regex SimpleCssIdSelectorRegex = new Regex(@"^#(?<name>(?<nmchar>[_a-z0-9-]|[\240-\377]|(?<escape>\\[^\r\n\f0-9a-f]))+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

/// <summary>
/// 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.
/// </summary>
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);

/// <summary>
/// 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.
/// - 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.
/// </summary>
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);

/// <summary>
/// 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.
/// </summary>
private static Regex SimpleCssEscapeCharacterRegex = new Regex(@"\\[^\r\n\f0-9a-f]", RegexOptions.Compiled | RegexOptions.IgnoreCase);

public PropertyCondition ParseCondition(ConditionFactory conditionFactory, string @using, string value)
{
switch (@using)
{
case "accessibility id":
return conditionFactory.ByAutomationId(value);
case "name":
return conditionFactory.ByName(value);
case "class name":
return conditionFactory.ByClassName(value);
case "link text":
return conditionFactory.ByText(value);
case "partial link text":
return conditionFactory.ByText(value, PropertyConditionFlags.MatchSubstring);
case "tag name":
return conditionFactory.ByControlType(Enum.Parse<ControlType>(value));
case "css selector":
var cssIdSelectorMatch = SimpleCssIdSelectorRegex.Match(value);
if (cssIdSelectorMatch.Success)
{
return conditionFactory.ByAutomationId(ReplaceCssEscapedCharacters(value.Substring(1)));
}
var cssClassSelectorMatch = SimpleCssClassSelectorRegex.Match(value);
if (cssClassSelectorMatch.Success)
{
return conditionFactory.ByClassName(ReplaceCssEscapedCharacters(value.Substring(1)));
}
var cssAttributeSelectorMatch = SimpleCssAttributeSelectorRegex.Match(value);
if (cssAttributeSelectorMatch.Success)
{
var attributeValue = ReplaceCssEscapedCharacters(cssAttributeSelectorMatch.Groups["string1value"].Success ?
cssAttributeSelectorMatch.Groups["string1value"].Value :
cssAttributeSelectorMatch.Groups["string2value"].Value);
if (cssAttributeSelectorMatch.Groups["ident"].Value == "name")
{
return conditionFactory.ByName(attributeValue);
}
}
throw WebDriverResponseException.UnsupportedOperation($"Selector strategy 'css selector' with value '{value}' is not supported");
default:
throw WebDriverResponseException.UnsupportedOperation($"Selector strategy '{@using}' is not supported");
}
}

private static string ReplaceCssEscapedCharacters(string value)
{
return SimpleCssEscapeCharacterRegex.Replace(value, match => match.Value.Substring(1));
}

}
}
9 changes: 9 additions & 0 deletions src/FlaUI.WebDriver/Services/IConditionParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using FlaUI.Core.Conditions;

namespace FlaUI.WebDriver.Services
{
public interface IConditionParser
{
PropertyCondition ParseCondition(ConditionFactory conditionFactory, string @using, string value);
}
}

0 comments on commit e5d543b

Please sign in to comment.