Skip to content

Commit a62b647

Browse files
authored
Merge pull request #139 from MADE-Apps/feature/pageobject-generator
Ported Legerity page object generator into main project repo
2 parents fd5387a + fdf2f99 commit a62b647

24 files changed

+956
-8
lines changed

.github/pull_request_template.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
## Fixes #
1+
## Resolves #
22
<!-- Add the issue ID after the '#' to automatically close the issue once the PR is merged -->
33

44
<!-- Please provide a description below of the changes made and how it has been tested -->
55

66
## PR checklist
77

8-
- [ ] Sample tests have been added/updated and pass
9-
- [ ] [Documentation](/docs) has been added/updated for changes
10-
- [ ] Code styling has been met on new source file changes
11-
- [ ] Contains **NO** breaking changes
8+
- [ ] Have Legerity sample tests been added or updated, run locally, and all pass
9+
- [ ] Have added or updated support for platform specific element wrappers been reflected in the Page Object Generator
10+
- [ ] Have code styling rules been run on all new source file changes
11+
- [ ] Have relevant articles in the docs been added or updated for all new source file changes
12+
- [ ] Have major breaking changes been made and are documented
1213

1314
<!-- If a breaking change has been made, please provide a detailed description below of the impact and the migration path -->
1415

1516
## Other information
16-
<!-- Please provide any additional information, links, or screenshots below if applicable -->
17+
<!-- Provide any additional information below that may be relevant to the changes made (e.g. app screenshots, documentation links, or existing PR reference) -->

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ on:
1111
- samples/**
1212
- tests/**
1313
- build/**
14+
- tools/**
1415
- .github/workflows/ci.yml
1516
pull_request:
1617
branches:
@@ -20,6 +21,7 @@ on:
2021
- samples/**
2122
- tests/**
2223
- build/**
24+
- tools/**
2325
- .github/workflows/ci.yml
2426
workflow_dispatch:
2527

@@ -52,7 +54,7 @@ jobs:
5254
uses: NuGet/setup-nuget@v1.0.5
5355

5456
- name: Restore dependencies
55-
run: nuget restore $SOLUTION
57+
run: dotnet restore $SOLUTION
5658

5759
- name: Build
5860
run: dotnet build $SOLUTION --configuration $BUILD_CONFIG -p:Version=$BUILD_VERSION --no-restore

samples/Directory.Build.props

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project>
2+
3+
<PropertyGroup>
4+
<Version>1.0.0.0</Version>
5+
<Authors>MADE Apps</Authors>
6+
<Company>MADE Apps</Company>
7+
<Copyright>Copyright (C) MADE Apps. All rights reserved.</Copyright>
8+
<NeutralLanguage>en</NeutralLanguage>
9+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
10+
<LangVersion>latest</LangVersion>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
15+
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive"/>
16+
</ItemGroup>
17+
18+
</Project>

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<PackageReleaseNotes>https://github.com/MADE-Apps/legerity/releases</PackageReleaseNotes>
1717
<NeutralLanguage>en</NeutralLanguage>
1818
<GenerateDocumentationFile>true</GenerateDocumentationFile>
19-
<LangVersion>8.0</LangVersion>
19+
<LangVersion>latest</LangVersion>
2020
</PropertyGroup>
2121

2222
<ItemGroup>

src/Legerity.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Legerity.IOS", "Legerity.IO
6161
EndProject
6262
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Legerity.Web", "Legerity.Web\Legerity.Web.csproj", "{66469000-1C91-4CBA-A27E-043C3E222DA6}"
6363
EndProject
64+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{9064E354-7A64-4288-93E5-B3628CA12DD3}"
65+
EndProject
66+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Legerity.PageObjectGenerator", "..\tools\Legerity.PageObjectGenerator\Legerity.PageObjectGenerator.csproj", "{063E6264-F623-4469-BADF-95C8E93E3ACF}"
67+
EndProject
6468
Global
6569
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6670
Debug|Any CPU = Debug|Any CPU
@@ -131,6 +135,10 @@ Global
131135
{66469000-1C91-4CBA-A27E-043C3E222DA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
132136
{66469000-1C91-4CBA-A27E-043C3E222DA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
133137
{66469000-1C91-4CBA-A27E-043C3E222DA6}.Release|Any CPU.Build.0 = Release|Any CPU
138+
{063E6264-F623-4469-BADF-95C8E93E3ACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
139+
{063E6264-F623-4469-BADF-95C8E93E3ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU
140+
{063E6264-F623-4469-BADF-95C8E93E3ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU
141+
{063E6264-F623-4469-BADF-95C8E93E3ACF}.Release|Any CPU.Build.0 = Release|Any CPU
134142
EndGlobalSection
135143
GlobalSection(SolutionProperties) = preSolution
136144
HideSolutionNode = FALSE
@@ -154,6 +162,7 @@ Global
154162
{EF10E4CE-62F1-41A9-807A-79A153CD7A04} = {158E9EAA-AEE0-4E0C-81E0-6E9E63341CC1}
155163
{9D100607-5873-419C-B1AD-8DE55E464CFE} = {44456B3E-73D9-43C5-9644-430E293ECD5E}
156164
{89BD06DC-7E77-4062-9149-EF95D3F9D999} = {1BA37704-10A3-4EDC-94AA-BF0D8CD11A9C}
165+
{063E6264-F623-4469-BADF-95C8E93E3ACF} = {9064E354-7A64-4288-93E5-B3628CA12DD3}
157166
EndGlobalSection
158167
GlobalSection(ExtensibilityGlobals) = postSolution
159168
SolutionGuid = {FAB58D66-B3F1-408A-A96F-572170771367}

tools/Directory.Build.props

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project>
2+
3+
<PropertyGroup>
4+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
5+
<EmbedUntrackedSources>true</EmbedUntrackedSources>
6+
<IncludeSymbols>true</IncludeSymbols>
7+
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
8+
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
9+
<Version>1.0.0.0</Version>
10+
<Authors>MADE Apps</Authors>
11+
<Company>MADE Apps</Company>
12+
<Copyright>Copyright (C) MADE Apps. All rights reserved.</Copyright>
13+
<PackageProjectUrl>https://github.com/MADE-Apps/legerity</PackageProjectUrl>
14+
<PackageLicenseFile>LICENSE</PackageLicenseFile>
15+
<PackageIcon>ProjectLogo.png</PackageIcon>
16+
<PackageReleaseNotes>https://github.com/MADE-Apps/legerity/releases</PackageReleaseNotes>
17+
<NeutralLanguage>en</NeutralLanguage>
18+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
19+
<LangVersion>latest</LangVersion>
20+
</PropertyGroup>
21+
22+
<ItemGroup>
23+
<None Include="..\..\assets\ProjectLogo.png" Pack="true" PackagePath=""/>
24+
<None Include="..\..\LICENSE" Pack="true" PackagePath=""/>
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
29+
</ItemGroup>
30+
31+
</Project>
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
namespace Legerity.Features.Generators.Android;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using System.Xml.Linq;
10+
using Infrastructure.IO;
11+
using Legerity.Features.Generators;
12+
using Legerity.Features.Generators.Models;
13+
using Legerity.Infrastructure.Extensions;
14+
using MADE.Collections.Compare;
15+
using MADE.Data.Validation.Extensions;
16+
using Scriban;
17+
using Serilog;
18+
19+
internal class AxmlPageObjectGenerator : IPageObjectGenerator
20+
{
21+
private const string AndroidNamespace = "http://schemas.android.com/apk/res/android";
22+
23+
private const string BaseElementType = "AndroidElement";
24+
25+
private static readonly GenericEqualityComparer<string> SimpleStringComparer = new(s => s.ToLower());
26+
27+
public static IEnumerable<string> SupportedCoreAndroidElements => new List<string>
28+
{
29+
"Button",
30+
"CheckBox",
31+
"DatePicker",
32+
"EditText",
33+
"RadioButton",
34+
"Spinner",
35+
"Switch",
36+
"TextView",
37+
"ToggleButton",
38+
"View"
39+
};
40+
41+
public async Task GenerateAsync(string ns, string inputPath, string outputPath)
42+
{
43+
IEnumerable<string>? filePaths = GetAxmlFilePaths(inputPath)?.ToList();
44+
45+
if (filePaths == null || !filePaths.Any())
46+
{
47+
Log.Warning("No AXML files found in {InputPath}", inputPath);
48+
return;
49+
}
50+
51+
foreach (string filePath in filePaths)
52+
{
53+
Log.Information($"Processing {filePath}...");
54+
55+
await using FileStream fileStream = File.Open(filePath, FileMode.Open);
56+
var axml = XDocument.Load(fileStream);
57+
58+
if (axml.Root != null)
59+
{
60+
var templateData =
61+
new GeneratorTemplateData(ns, Path.GetFileNameWithoutExtension(filePath), BaseElementType);
62+
63+
Log.Information($"Generating template for {templateData}...");
64+
65+
IEnumerable<XElement> elements = this.FlattenElements(axml.Root.Elements());
66+
foreach (XElement element in elements)
67+
{
68+
string? id = RemoveAndroidIdReference(element.Attribute(XName.Get("id", AndroidNamespace))?.Value);
69+
string? contentDesc = element.Attribute(XName.Get("contentDescription", AndroidNamespace))?.Value;
70+
71+
string? byLocatorType = GetByLocatorType(id, contentDesc);
72+
if (byLocatorType == null || byLocatorType.IsNullOrWhiteSpace())
73+
{
74+
continue;
75+
}
76+
77+
string? byQueryValue = id ?? contentDesc;
78+
if (byQueryValue == null || byQueryValue.IsNullOrWhiteSpace())
79+
{
80+
continue;
81+
}
82+
83+
var uiElement = new UiElement(
84+
GetElementWrapperType(element.Name.LocalName),
85+
byQueryValue.Capitalize(),
86+
byLocatorType,
87+
byQueryValue);
88+
89+
Log.Information($"Element found on page - {uiElement}");
90+
91+
templateData.Trait = uiElement;
92+
templateData.Elements.Add(uiElement);
93+
}
94+
95+
await GeneratePageObjectClassFileAsync(templateData, outputPath);
96+
}
97+
else
98+
{
99+
Log.Warning($"Skipping {filePath} as a page was not detected");
100+
}
101+
}
102+
}
103+
104+
private static string? RemoveAndroidIdReference(string? value)
105+
{
106+
return value == null || string.IsNullOrWhiteSpace(value)
107+
? null
108+
: value.Replace("+", string.Empty).Replace("@id/", string.Empty);
109+
}
110+
111+
private static async Task GeneratePageObjectClassFileAsync(
112+
GeneratorTemplateData templateData,
113+
string outputFolder)
114+
{
115+
var pageObjectTemplate = Template.Parse(await EmbeddedResourceLoader.ReadAsync("Legerity.Templates.AndroidPageObject.template"));
116+
117+
string outputFile = $"{templateData.Page}.cs";
118+
119+
Log.Information($"Generating {outputFile} page object file...");
120+
string result = await pageObjectTemplate.RenderAsync(templateData);
121+
122+
FileStream output = File.Create(Path.Combine(outputFolder, outputFile));
123+
var outputWriter = new StreamWriter(output, Encoding.UTF8);
124+
125+
await using (outputWriter)
126+
{
127+
await outputWriter.WriteAsync(result);
128+
}
129+
}
130+
131+
private static string? GetByLocatorType(string? id, string? contentDesc)
132+
{
133+
if (id != null && !id.IsNullOrWhiteSpace())
134+
{
135+
return "Id";
136+
}
137+
138+
return contentDesc != null && !contentDesc.IsNullOrWhiteSpace() ? "AndroidContentDesc" : null;
139+
}
140+
141+
private static IEnumerable<string>? GetAxmlFilePaths(string searchFolder)
142+
{
143+
string[]? filePaths = default;
144+
145+
try
146+
{
147+
filePaths = Directory.GetFiles(searchFolder, "*.axml", SearchOption.AllDirectories);
148+
}
149+
catch (UnauthorizedAccessException)
150+
{
151+
Log.Error("An error occurred while retrieving AXML files for processing");
152+
}
153+
154+
return filePaths;
155+
}
156+
157+
private static string GetElementWrapperType(string elementName)
158+
{
159+
return SupportedCoreAndroidElements.Contains(elementName, SimpleStringComparer) ? elementName : BaseElementType;
160+
}
161+
162+
private IEnumerable<XElement> FlattenElements(IEnumerable<XElement> elements)
163+
{
164+
return elements.SelectMany(c => this.FlattenElements(c.Elements())).Concat(elements);
165+
}
166+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Legerity.Features.Generators;
2+
3+
using System.Threading.Tasks;
4+
5+
internal interface IPageObjectGenerator
6+
{
7+
Task GenerateAsync(string ns, string inputPath, string outputPath);
8+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Legerity.Features.Generators.Models;
2+
3+
using System.Collections.Generic;
4+
5+
internal class GeneratorTemplateData
6+
{
7+
public GeneratorTemplateData(string ns, string page, string baseElementType)
8+
{
9+
this.Namespace = ns;
10+
this.Page = page;
11+
this.Type = baseElementType;
12+
}
13+
14+
public string Page { get; set; }
15+
16+
public string Type { get; set; }
17+
18+
public string Namespace { get; set; }
19+
20+
public UiElement Trait { get; set; }
21+
22+
public List<UiElement> Elements { get; set; } = new();
23+
24+
public override string ToString()
25+
{
26+
return $"[Page] {this.Page};";
27+
}
28+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Legerity.Features.Generators.Models;
2+
3+
internal class UiElement
4+
{
5+
public UiElement(string type, string name, string by, string value)
6+
{
7+
this.Type = type;
8+
this.Name = name;
9+
this.By = by;
10+
this.Value = value;
11+
}
12+
13+
public string Type { get; set; }
14+
15+
public string Name { get; set; }
16+
17+
public string By { get; set; }
18+
19+
public string Value { get; set; }
20+
21+
public override string ToString()
22+
{
23+
return $"[Type] {this.Type}; [Name] {this.Name}; [By] {this.By}; [Value] {this.Value};";
24+
}
25+
}

0 commit comments

Comments
 (0)