diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index cba70dc1..e103b01d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,16 +1,17 @@ -## Fixes # +## Resolves # ## PR checklist -- [ ] Sample tests have been added/updated and pass -- [ ] [Documentation](/docs) has been added/updated for changes -- [ ] Code styling has been met on new source file changes -- [ ] Contains **NO** breaking changes +- [ ] Have Legerity sample tests been added or updated, run locally, and all pass +- [ ] Have added or updated support for platform specific element wrappers been reflected in the Page Object Generator +- [ ] Have code styling rules been run on all new source file changes +- [ ] Have relevant articles in the docs been added or updated for all new source file changes +- [ ] Have major breaking changes been made and are documented ## Other information - + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b763ac5e..2229bb3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ on: - samples/** - tests/** - build/** + - tools/** - .github/workflows/ci.yml pull_request: branches: @@ -20,6 +21,7 @@ on: - samples/** - tests/** - build/** + - tools/** - .github/workflows/ci.yml workflow_dispatch: @@ -52,7 +54,7 @@ jobs: uses: NuGet/setup-nuget@v1.0.5 - name: Restore dependencies - run: nuget restore $SOLUTION + run: dotnet restore $SOLUTION - name: Build run: dotnet build $SOLUTION --configuration $BUILD_CONFIG -p:Version=$BUILD_VERSION --no-restore diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props new file mode 100644 index 00000000..7944bc5d --- /dev/null +++ b/samples/Directory.Build.props @@ -0,0 +1,18 @@ + + + + 1.0.0.0 + MADE Apps + MADE Apps + Copyright (C) MADE Apps. All rights reserved. + en + true + latest + + + + + + + + \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1abb5ac7..270fbcdd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -16,7 +16,7 @@ https://github.com/MADE-Apps/legerity/releases en true - 8.0 + latest diff --git a/src/Legerity.sln b/src/Legerity.sln index 23d2a341..4c01cfc5 100644 --- a/src/Legerity.sln +++ b/src/Legerity.sln @@ -61,6 +61,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Legerity.IOS", "Legerity.IO EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Legerity.Web", "Legerity.Web\Legerity.Web.csproj", "{66469000-1C91-4CBA-A27E-043C3E222DA6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{9064E354-7A64-4288-93E5-B3628CA12DD3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Legerity.PageObjectGenerator", "..\tools\Legerity.PageObjectGenerator\Legerity.PageObjectGenerator.csproj", "{063E6264-F623-4469-BADF-95C8E93E3ACF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -131,6 +135,10 @@ Global {66469000-1C91-4CBA-A27E-043C3E222DA6}.Debug|Any CPU.Build.0 = Debug|Any CPU {66469000-1C91-4CBA-A27E-043C3E222DA6}.Release|Any CPU.ActiveCfg = Release|Any CPU {66469000-1C91-4CBA-A27E-043C3E222DA6}.Release|Any CPU.Build.0 = Release|Any CPU + {063E6264-F623-4469-BADF-95C8E93E3ACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {063E6264-F623-4469-BADF-95C8E93E3ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {063E6264-F623-4469-BADF-95C8E93E3ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {063E6264-F623-4469-BADF-95C8E93E3ACF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -154,6 +162,7 @@ Global {EF10E4CE-62F1-41A9-807A-79A153CD7A04} = {158E9EAA-AEE0-4E0C-81E0-6E9E63341CC1} {9D100607-5873-419C-B1AD-8DE55E464CFE} = {44456B3E-73D9-43C5-9644-430E293ECD5E} {89BD06DC-7E77-4062-9149-EF95D3F9D999} = {1BA37704-10A3-4EDC-94AA-BF0D8CD11A9C} + {063E6264-F623-4469-BADF-95C8E93E3ACF} = {9064E354-7A64-4288-93E5-B3628CA12DD3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FAB58D66-B3F1-408A-A96F-572170771367} diff --git a/tools/Directory.Build.props b/tools/Directory.Build.props new file mode 100644 index 00000000..8b2358ab --- /dev/null +++ b/tools/Directory.Build.props @@ -0,0 +1,31 @@ + + + + true + true + true + snupkg + true + 1.0.0.0 + MADE Apps + MADE Apps + Copyright (C) MADE Apps. All rights reserved. + https://github.com/MADE-Apps/legerity + LICENSE + ProjectLogo.png + https://github.com/MADE-Apps/legerity/releases + en + true + latest + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Features/Generators/Android/AxmlPageObjectGenerator.cs b/tools/Legerity.PageObjectGenerator/Features/Generators/Android/AxmlPageObjectGenerator.cs new file mode 100644 index 00000000..29bbe18d --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Features/Generators/Android/AxmlPageObjectGenerator.cs @@ -0,0 +1,166 @@ +namespace Legerity.Features.Generators.Android; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Infrastructure.IO; +using Legerity.Features.Generators; +using Legerity.Features.Generators.Models; +using Legerity.Infrastructure.Extensions; +using MADE.Collections.Compare; +using MADE.Data.Validation.Extensions; +using Scriban; +using Serilog; + +internal class AxmlPageObjectGenerator : IPageObjectGenerator +{ + private const string AndroidNamespace = "http://schemas.android.com/apk/res/android"; + + private const string BaseElementType = "AndroidElement"; + + private static readonly GenericEqualityComparer SimpleStringComparer = new(s => s.ToLower()); + + public static IEnumerable SupportedCoreAndroidElements => new List + { + "Button", + "CheckBox", + "DatePicker", + "EditText", + "RadioButton", + "Spinner", + "Switch", + "TextView", + "ToggleButton", + "View" + }; + + public async Task GenerateAsync(string ns, string inputPath, string outputPath) + { + IEnumerable? filePaths = GetAxmlFilePaths(inputPath)?.ToList(); + + if (filePaths == null || !filePaths.Any()) + { + Log.Warning("No AXML files found in {InputPath}", inputPath); + return; + } + + foreach (string filePath in filePaths) + { + Log.Information($"Processing {filePath}..."); + + await using FileStream fileStream = File.Open(filePath, FileMode.Open); + var axml = XDocument.Load(fileStream); + + if (axml.Root != null) + { + var templateData = + new GeneratorTemplateData(ns, Path.GetFileNameWithoutExtension(filePath), BaseElementType); + + Log.Information($"Generating template for {templateData}..."); + + IEnumerable elements = this.FlattenElements(axml.Root.Elements()); + foreach (XElement element in elements) + { + string? id = RemoveAndroidIdReference(element.Attribute(XName.Get("id", AndroidNamespace))?.Value); + string? contentDesc = element.Attribute(XName.Get("contentDescription", AndroidNamespace))?.Value; + + string? byLocatorType = GetByLocatorType(id, contentDesc); + if (byLocatorType == null || byLocatorType.IsNullOrWhiteSpace()) + { + continue; + } + + string? byQueryValue = id ?? contentDesc; + if (byQueryValue == null || byQueryValue.IsNullOrWhiteSpace()) + { + continue; + } + + var uiElement = new UiElement( + GetElementWrapperType(element.Name.LocalName), + byQueryValue.Capitalize(), + byLocatorType, + byQueryValue); + + Log.Information($"Element found on page - {uiElement}"); + + templateData.Trait = uiElement; + templateData.Elements.Add(uiElement); + } + + await GeneratePageObjectClassFileAsync(templateData, outputPath); + } + else + { + Log.Warning($"Skipping {filePath} as a page was not detected"); + } + } + } + + private static string? RemoveAndroidIdReference(string? value) + { + return value == null || string.IsNullOrWhiteSpace(value) + ? null + : value.Replace("+", string.Empty).Replace("@id/", string.Empty); + } + + private static async Task GeneratePageObjectClassFileAsync( + GeneratorTemplateData templateData, + string outputFolder) + { + var pageObjectTemplate = Template.Parse(await EmbeddedResourceLoader.ReadAsync("Legerity.Templates.AndroidPageObject.template")); + + string outputFile = $"{templateData.Page}.cs"; + + Log.Information($"Generating {outputFile} page object file..."); + string result = await pageObjectTemplate.RenderAsync(templateData); + + FileStream output = File.Create(Path.Combine(outputFolder, outputFile)); + var outputWriter = new StreamWriter(output, Encoding.UTF8); + + await using (outputWriter) + { + await outputWriter.WriteAsync(result); + } + } + + private static string? GetByLocatorType(string? id, string? contentDesc) + { + if (id != null && !id.IsNullOrWhiteSpace()) + { + return "Id"; + } + + return contentDesc != null && !contentDesc.IsNullOrWhiteSpace() ? "AndroidContentDesc" : null; + } + + private static IEnumerable? GetAxmlFilePaths(string searchFolder) + { + string[]? filePaths = default; + + try + { + filePaths = Directory.GetFiles(searchFolder, "*.axml", SearchOption.AllDirectories); + } + catch (UnauthorizedAccessException) + { + Log.Error("An error occurred while retrieving AXML files for processing"); + } + + return filePaths; + } + + private static string GetElementWrapperType(string elementName) + { + return SupportedCoreAndroidElements.Contains(elementName, SimpleStringComparer) ? elementName : BaseElementType; + } + + private IEnumerable FlattenElements(IEnumerable elements) + { + return elements.SelectMany(c => this.FlattenElements(c.Elements())).Concat(elements); + } +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Features/Generators/IPageObjectGenerator.cs b/tools/Legerity.PageObjectGenerator/Features/Generators/IPageObjectGenerator.cs new file mode 100644 index 00000000..4d40d973 --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Features/Generators/IPageObjectGenerator.cs @@ -0,0 +1,8 @@ +namespace Legerity.Features.Generators; + +using System.Threading.Tasks; + +internal interface IPageObjectGenerator +{ + Task GenerateAsync(string ns, string inputPath, string outputPath); +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Features/Generators/Models/GeneratorTemplateData.cs b/tools/Legerity.PageObjectGenerator/Features/Generators/Models/GeneratorTemplateData.cs new file mode 100644 index 00000000..87cdbf7e --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Features/Generators/Models/GeneratorTemplateData.cs @@ -0,0 +1,28 @@ +namespace Legerity.Features.Generators.Models; + +using System.Collections.Generic; + +internal class GeneratorTemplateData +{ + public GeneratorTemplateData(string ns, string page, string baseElementType) + { + this.Namespace = ns; + this.Page = page; + this.Type = baseElementType; + } + + public string Page { get; set; } + + public string Type { get; set; } + + public string Namespace { get; set; } + + public UiElement Trait { get; set; } + + public List Elements { get; set; } = new(); + + public override string ToString() + { + return $"[Page] {this.Page};"; + } +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Features/Generators/Models/UiElement.cs b/tools/Legerity.PageObjectGenerator/Features/Generators/Models/UiElement.cs new file mode 100644 index 00000000..8feb7c6d --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Features/Generators/Models/UiElement.cs @@ -0,0 +1,25 @@ +namespace Legerity.Features.Generators.Models; + +internal class UiElement +{ + public UiElement(string type, string name, string by, string value) + { + this.Type = type; + this.Name = name; + this.By = by; + this.Value = value; + } + + public string Type { get; set; } + + public string Name { get; set; } + + public string By { get; set; } + + public string Value { get; set; } + + public override string ToString() + { + return $"[Type] {this.Type}; [Name] {this.Name}; [By] {this.By}; [Value] {this.Value};"; + } +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Features/Generators/Windows/XamlPageObjectGenerator.cs b/tools/Legerity.PageObjectGenerator/Features/Generators/Windows/XamlPageObjectGenerator.cs new file mode 100644 index 00000000..14c73c18 --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Features/Generators/Windows/XamlPageObjectGenerator.cs @@ -0,0 +1,183 @@ +namespace Legerity.Features.Generators.Windows; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Infrastructure.IO; +using Legerity.Features.Generators; +using Legerity.Features.Generators.Models; +using Legerity.Infrastructure.Extensions; +using MADE.Collections.Compare; +using MADE.Data.Validation.Extensions; +using Scriban; +using Serilog; + +internal class XamlPageObjectGenerator : IPageObjectGenerator +{ + private const string XamlNamespace = "http://schemas.microsoft.com/winfx/2006/xaml"; + + private const string BaseElementType = "WindowsElement"; + + private static readonly GenericEqualityComparer SimpleStringComparer = new(s => s.ToLower()); + + public static IEnumerable SupportedCoreWindowsElements => new List + { + "AppBarButton", + "AppBarToggleButton", + "AutoSuggestBox", + "Button", + "CalendarDatePicker", + "CalendarView", + "CheckBox", + "ComboBox", + "CommandBar", + "DatePicker", + "FlipView", + "GridView", + "Hub", + "HyperlinkButton", + "InkToolbar", + "ListBox", + "ListView", + "MenuFlyoutItem", + "MenuFlyoutSubItem", + "PasswordBox", + "Pivot", + "ProgressBar", + "ProgressRing", + "RadioButton", + "Slider", + "TextBlock", + "TextBox", + "TimePicker", + "ToggleButton", + "ToggleSwitch" + }; + + public async Task GenerateAsync(string ns, string inputPath, string outputPath) + { + IEnumerable? filePaths = GetXamlFilePaths(inputPath)?.ToList(); + + if (filePaths == null || !filePaths.Any()) + { + Log.Warning("No XAML files found in {InputPath}", inputPath); + return; + } + + foreach (string filePath in filePaths) + { + Log.Information($"Processing {filePath}..."); + + await using FileStream fileStream = File.Open(filePath, FileMode.Open); + var xaml = XDocument.Load(fileStream); + + if (xaml.Root != null && xaml.Root.Name.ToString().Contains("Page")) + { + var templateData = + new GeneratorTemplateData(ns, Path.GetFileNameWithoutExtension(filePath), BaseElementType); + + Log.Information($"Generating template for {templateData}..."); + + IEnumerable elements = this.FlattenElements(xaml.Root.Elements()); + foreach (XElement element in elements) + { + string? automationId = element.Attribute("AutomationProperties.AutomationId")?.Value; + string? uid = element.Attribute(XName.Get("Uid", XamlNamespace))?.Value; + string? name = element.Attribute(XName.Get("Name", XamlNamespace))?.Value; + + string? byLocatorType = GetByLocatorType(uid, automationId, name); + + if (byLocatorType == null || byLocatorType.IsNullOrWhiteSpace()) + { + continue; + } + + string? wrapperAutomationId = uid ?? automationId; + string? byQueryValue = wrapperAutomationId ?? name; + + if (byQueryValue == null || byQueryValue.IsNullOrWhiteSpace()) + { + continue; + } + + var uiElement = new UiElement( + GetElementWrapperType(element.Name.LocalName), + byQueryValue.Capitalize(), + byLocatorType, + byQueryValue); + + Log.Information($"Element found on page - {uiElement}"); + + templateData.Trait = uiElement; + templateData.Elements.Add(uiElement); + } + + await GeneratePageObjectClassFileAsync(templateData, outputPath); + } + else + { + Log.Warning($"Skipping {filePath} as a page was not detected"); + } + } + } + + private static async Task GeneratePageObjectClassFileAsync( + GeneratorTemplateData templateData, + string outputFolder) + { + var pageObjectTemplate = Template.Parse(await EmbeddedResourceLoader.ReadAsync("Legerity.Templates.WindowsPageObject.template")); + + string outputFile = $"{templateData.Page}.cs"; + + Log.Information($"Generating {outputFile} page object file..."); + string result = await pageObjectTemplate.RenderAsync(templateData); + + FileStream output = File.Create(Path.Combine(outputFolder, outputFile)); + var outputWriter = new StreamWriter(output, Encoding.UTF8); + + await using (outputWriter) + { + await outputWriter.WriteAsync(result); + } + } + + private static string? GetByLocatorType(string? uid, string? automationId, string? name) + { + if ((uid != null && !uid.IsNullOrWhiteSpace()) || (automationId != null && !automationId.IsNullOrWhiteSpace())) + { + return "AutomationId"; + } + + return name != null && !name.IsNullOrWhiteSpace() ? "Name" : null; + } + + private static IEnumerable? GetXamlFilePaths(string searchFolder) + { + string[]? filePaths = default; + + try + { + filePaths = Directory.GetFiles(searchFolder, "*.xaml", SearchOption.AllDirectories); + } + catch (UnauthorizedAccessException) + { + Log.Error("An error occurred while retrieving XAML files for processing"); + } + + return filePaths; + } + + private static string GetElementWrapperType(string elementName) + { + return SupportedCoreWindowsElements.Contains(elementName, SimpleStringComparer) ? elementName : BaseElementType; + } + + private IEnumerable FlattenElements(IEnumerable elements) + { + return elements.SelectMany(c => this.FlattenElements(c.Elements())).Concat(elements); + } +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Infrastructure/Configuration/Options.cs b/tools/Legerity.PageObjectGenerator/Infrastructure/Configuration/Options.cs new file mode 100644 index 00000000..05f0bc37 --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Infrastructure/Configuration/Options.cs @@ -0,0 +1,25 @@ +namespace Legerity.Infrastructure.Configuration; + +using System; +using CommandLine; + +internal class Options +{ + [Option('i', "input", + HelpText = + "The path to the folder where platform pages exist that will be generating page objects for. Default to the executing folder.")] + public string InputPath { get; set; } = Environment.CurrentDirectory; + + [Option('o', "output", + HelpText = + "The path to the folder where the generated class files should be stored. Default to the 'Generated' folder in the executing folder.")] + public string OutputPath { get; set; } = System.IO.Path.Combine(Environment.CurrentDirectory, "Generated"); + + [Option('n', "namespace", + HelpText = + "The namespace to apply to the output page objects. Default to 'LegerityTests.Pages'.")] + public string Namespace { get; set; } = "LegerityTests.Pages"; + + [Option('p', "platform", Required = true, HelpText = "The platform that will be generating page objects for.")] + public PlatformType PlatformType { get; set; } +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Infrastructure/Configuration/PlatformType.cs b/tools/Legerity.PageObjectGenerator/Infrastructure/Configuration/PlatformType.cs new file mode 100644 index 00000000..c640b331 --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Infrastructure/Configuration/PlatformType.cs @@ -0,0 +1,9 @@ +namespace Legerity.Infrastructure.Configuration; + +internal enum PlatformType +{ + Windows, + Android, + IOS, + Web +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Infrastructure/Extensions/StringExtensions.cs b/tools/Legerity.PageObjectGenerator/Infrastructure/Extensions/StringExtensions.cs new file mode 100644 index 00000000..30b4d0f0 --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Infrastructure/Extensions/StringExtensions.cs @@ -0,0 +1,11 @@ +namespace Legerity.Infrastructure.Extensions; + +using System.Linq; + +internal static class StringExtensions +{ + internal static string Capitalize(this string value) + { + return value.First().ToString().ToUpper() + value.Substring(1); + } +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Infrastructure/IO/EmbeddedResourceLoader.cs b/tools/Legerity.PageObjectGenerator/Infrastructure/IO/EmbeddedResourceLoader.cs new file mode 100644 index 00000000..209ac24a --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Infrastructure/IO/EmbeddedResourceLoader.cs @@ -0,0 +1,20 @@ +namespace Legerity.Infrastructure.IO +{ + using System.Reflection; + + internal static class EmbeddedResourceLoader + { + internal static Task ReadAsync(string fileName) + { + var assembly = Assembly.GetExecutingAssembly(); + using Stream? stream = assembly.GetManifestResourceStream(fileName); + if (stream == null) + { + return Task.FromResult(default(string)); + } + + using var reader = new StreamReader(stream); + return reader.ReadToEndAsync()!; + } + } +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Infrastructure/Logging/SerilogConfigurator.cs b/tools/Legerity.PageObjectGenerator/Infrastructure/Logging/SerilogConfigurator.cs new file mode 100644 index 00000000..5c804f77 --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Infrastructure/Logging/SerilogConfigurator.cs @@ -0,0 +1,20 @@ +namespace Legerity.Infrastructure.Logging; + +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; + +internal static class SerilogConfigurator +{ + private const string LoggingMessageTemplate = "[{Level}] {Message:lj}{NewLine}"; + + public static void ConfigureLogging() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console(outputTemplate: LoggingMessageTemplate, + theme: AnsiConsoleTheme.Literate, + restrictedToMinimumLevel: LogEventLevel.Debug) + .CreateLogger(); + } +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/Legerity.PageObjectGenerator.csproj b/tools/Legerity.PageObjectGenerator/Legerity.PageObjectGenerator.csproj new file mode 100644 index 00000000..6129c55e --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Legerity.PageObjectGenerator.csproj @@ -0,0 +1,42 @@ + + + + true + Exe + net6.0 + enable + enable + true + legerity-pop + Legerity Page Object Generator + A command line tool for auto generating page objects from Windows, Android, iOS, and web pages for UI tests using the Legerity framework + README.md + Legerity + PageObject Appium Selenium WindowsDriver WinAppDriver Windows UWP Web Android IOS Xamarin Uno + + + + + + + + + + + Never + + + Never + + + + + + + + + + + + + diff --git a/tools/Legerity.PageObjectGenerator/Program.cs b/tools/Legerity.PageObjectGenerator/Program.cs new file mode 100644 index 00000000..6302fd8b --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Program.cs @@ -0,0 +1,69 @@ +namespace Legerity; + +using System.IO; +using System.Threading.Tasks; +using CommandLine; +using Features.Generators.Android; +using Features.Generators.Windows; +using Infrastructure.Configuration; +using Infrastructure.Logging; +using Legerity.Features.Generators; +using Serilog; + +public class Program +{ + public static async Task Main(string[] args) + { + SerilogConfigurator.ConfigureLogging(); + await Parser.Default.ParseArguments(args) + .WithNotParsed(errors => + { + foreach (Error error in errors) + { + if (error.Tag == ErrorType.MissingRequiredOptionError) + { + Log.Error("Missing required option: {0}", error.ToString()); + } + } + }) + .WithParsedAsync(async options => + { + Log.Information($"Locating {options.PlatformType:G} page files in {options.InputPath}..."); + + IPageObjectGenerator? pageObjectGenerator = default; + + switch (options.PlatformType) + { + case PlatformType.Windows: + pageObjectGenerator = new XamlPageObjectGenerator(); + break; + case PlatformType.Android: + pageObjectGenerator = new AxmlPageObjectGenerator(); + break; + case PlatformType.Web: + Log.Warning("Web page object generation is not currently supported!"); + break; + case PlatformType.IOS: + Log.Warning("iOS page object generation is not currently supported!"); + break; + default: + Log.Warning("Cannot generate Legerity page objects for an unsupported platform type!"); + return; + } + + if (pageObjectGenerator == default) + { + return; + } + + if (!Directory.Exists(options.OutputPath)) + { + Directory.CreateDirectory(options.OutputPath); + } + + await pageObjectGenerator.GenerateAsync(options.Namespace, options.InputPath, options.OutputPath); + + Log.Information("Finished generating Legerity page objects!"); + }); + } +} \ No newline at end of file diff --git a/tools/Legerity.PageObjectGenerator/README.md b/tools/Legerity.PageObjectGenerator/README.md new file mode 100644 index 00000000..b6f4a78f --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/README.md @@ -0,0 +1,54 @@ +# Legerity page object generator CLI tool + +[![GitHub release](https://img.shields.io/github/release/MADE-Apps/legerity.svg)](https://github.com/MADE-Apps/legerity/releases) +[![Build status](https://github.com/MADE-Apps/legerity/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/MADE-Apps/legerity/actions/workflows/ci.yml) +[![Twitter Followers](https://img.shields.io/twitter/follow/jamesmcroft?label=follow%20%40jamesmcroft&style=flat)](https://twitter.com/jamesmcroft) +[![.NET Tool](https://img.shields.io/nuget/v/Legerity.PageObjectGenerator)](https://www.nuget.org/packages/Legerity.PageObjectGenerator/) + +Want to generate your Legerity page objects super fast? 🚀 + +The Legerity page object generator CLI tool allows you to auto-generate page objects for your Windows and Android application based on page files. (iOS and web coming soon!) + +Just provide an input path, output path, and output namespace and you're away! 🤩 + +## Getting started + +### Install the tool + +```bash +dotnet tool install -g Legerity.PageObjectGenerator +``` + +**Or update an existing install** + +```bash +dotnet tool update -g Legerity.PageObjectGenerator +``` + +### Run the tool + +Once you have the tool installed, it is simply a case of running the CLI and providing the input, output, and namespace arguments. + +```bash +legerity-pop -i "path/to/input/folder" -o "path/to/output/folder" -n "My.Namespace" -p "Windows" +``` + +This will read through all the page files that can be found under the input folder, generate a Legerity `BasePage` equivalent based on supported elements, and then drop those into your output folder. And that's it! + +**Platform argument supports:** Windows / Android + +## Contributing 🤝🏻 + +Contributions, issues and feature requests are welcome! + +Feel free to check the [issues page](https://github.com/MADE-Apps/legerity/issues). You can also take a look at the [contributing guide](https://github.com/MADE-Apps/legerity/blob/main/CONTRIBUTING.md). + +We actively encourage you to jump in and help with any issues, and if you find one, don't forget to log it! + +## Support this project 💗 + +As many developers know, projects like Legerity are built and maintained in spare time. If you find this project useful, please **Star** the repo and if possible, [sponsor the project development on GitHub](https://github.com/sponsors/jamesmcroft). + +## License + +This project is made available under the terms and conditions of the [MIT license](https://github.com/MADE-Apps/legerity/blob/main/LICENSE). diff --git a/tools/Legerity.PageObjectGenerator/Templates/AndroidPageObject.template b/tools/Legerity.PageObjectGenerator/Templates/AndroidPageObject.template new file mode 100644 index 00000000..3ca137ac --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Templates/AndroidPageObject.template @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Legerity.Pages; +using Legerity.Android; +using Legerity.Android.Elements.Core; +using Legerity.Android.Extensions; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Android; +using OpenQA.Selenium.Appium.Android.UiAutomator; +using OpenQA.Selenium.Remote; + +namespace {{namespace}} +{ + /// + /// Defines a Legerity page object that represents the application {{page}}. + /// + public class {{page}} : BasePage + { + /// + /// Gets a given trait of the page to verify that the page is in view. + /// + protected override By Trait => {{- if string.contains trait.by "Id"}} By.Id("{{trait.value}}"); {{- else if string.contains trait.by "AndroidContentDesc"}} AndroidByExtras.ContentDescription("{{trait.value}}"); {{- end}} + {{~ for element in elements }} + /// + /// Gets the {{element.type}} that represents the {{element.name}} UI element of the page. + /// + public {{element.type}} {{element.name}} => App.FindElement({{- if string.contains element.by "Id"}}By.Id("{{element.value}}") {{- else if string.contains element.by "AndroidContentDesc"}}AndroidByExtras.ContentDescription("{{element.value}}") {{- end}}) as {{type}}; + {{~ end ~}} + } +} diff --git a/tools/Legerity.PageObjectGenerator/Templates/WindowsPageObject.template b/tools/Legerity.PageObjectGenerator/Templates/WindowsPageObject.template new file mode 100644 index 00000000..8318ef26 --- /dev/null +++ b/tools/Legerity.PageObjectGenerator/Templates/WindowsPageObject.template @@ -0,0 +1,33 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Legerity.Pages; +using Legerity.Windows; +using Legerity.Windows.Elements.Core; +using Legerity.Windows.Extensions; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Windows; +using OpenQA.Selenium.Remote; + +namespace {{namespace}} +{ + /// + /// Defines a Legerity page object that represents the application {{page}}. + /// + public class {{page}} : BasePage + { + /// + /// Gets a given trait of the page to verify that the page is in view. + /// + protected override By Trait => {{- if string.contains trait.by "Name"}} By.Name("{{trait.value}}"); {{- else if string.contains trait.by "AutomationId"}} WindowsByExtras.AutomationId("{{trait.value}}"); {{- end}} + {{~ for element in elements }} + /// + /// Gets the {{element.type}} that represents the {{element.name}} UI element of the page. + /// + public {{element.type}} {{element.name}} => App.FindElement({{- if string.contains element.by "Name"}}By.Name("{{element.value}}") {{- else if string.contains element.by "AutomationId"}}WindowsByExtras.AutomationId("{{element.value}}") {{- end}}) as {{type}}; + {{~ end ~}} + } +} diff --git a/tools/PageObjectSamples/Android/FormInputLayout.axml b/tools/PageObjectSamples/Android/FormInputLayout.axml new file mode 100644 index 00000000..a7b1498d --- /dev/null +++ b/tools/PageObjectSamples/Android/FormInputLayout.axml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/PageObjectSamples/Windows/App.xaml b/tools/PageObjectSamples/Windows/App.xaml new file mode 100644 index 00000000..d875286a --- /dev/null +++ b/tools/PageObjectSamples/Windows/App.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/tools/PageObjectSamples/Windows/MainPage.xaml b/tools/PageObjectSamples/Windows/MainPage.xaml new file mode 100644 index 00000000..7ebfac8f --- /dev/null +++ b/tools/PageObjectSamples/Windows/MainPage.xaml @@ -0,0 +1,93 @@ + + + + + + + + +