7 Commits

290 changed files with 2442 additions and 1651 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_CALM_MUD_04916B210 }} azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_CALM_MUD_04916B210 }}
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload" action: "upload"
app_location: "IGP" app_location: "Web"
api_location: "" api_location: ""
output_location: "./wwwroot" output_location: "./wwwroot"
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_NICE_COAST_0F8B08010 }} azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_NICE_COAST_0F8B08010 }}
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload" action: "upload"
app_location: "IGP" app_location: "Web"
api_location: "" api_location: ""
output_location: "./wwwroot" output_location: "./wwwroot"
+4
View File
@@ -137,6 +137,7 @@ DocProject/Help/html
# Click-Once directory # Click-Once directory
publish/ publish/
publish_release/
# Publish Web Output # Publish Web Output
*.[Pp]ublish.xml *.[Pp]ublish.xml
@@ -264,3 +265,6 @@ __pycache__/
**/.vs/ **/.vs/
.DS_Store .DS_Store
publish_release/
View File
+1
View File
@@ -0,0 +1 @@
3.0
View File
+3 -3
View File
@@ -7,7 +7,7 @@
"type": "process", "type": "process",
"args": [ "args": [
"build", "build",
"${workspaceFolder}/IGP/IGP.csproj", "${workspaceFolder}/Web/Web.csproj",
"/property:GenerateFullPaths=true", "/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary" "/consoleloggerparameters:NoSummary"
], ],
@@ -19,7 +19,7 @@
"type": "process", "type": "process",
"args": [ "args": [
"publish", "publish",
"${workspaceFolder}/IGP/IGP.csproj", "${workspaceFolder}/Web/Web.csproj",
"/property:GenerateFullPaths=true", "/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary" "/consoleloggerparameters:NoSummary"
], ],
@@ -33,7 +33,7 @@
"watch", "watch",
"run", "run",
"--project", "--project",
"${workspaceFolder}/IGP/IGP.csproj" "${workspaceFolder}/Web/Web.csproj"
], ],
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
} }
+16
View File
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>IGP.Calculator.Cli</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Services\Services.csproj" />
<ProjectReference Include="..\Model\Model.csproj" />
</ItemGroup>
</Project>
+155
View File
@@ -0,0 +1,155 @@
using IGP.Calculator.Cli.Services;
using Model.Entity;
using Model.Entity.Data;
using Services.Immortal;
using Services.Website;
var faction = DataType.FACTION_QRath;
var immortal = DataType.IMMORTAL_Orzum;
var attackTime = 1500;
var entityNames = new List<string>();
for (var i = 0; i < args.Length; i++)
switch (args[i].ToLower())
{
case "--faction" when i + 1 < args.Length:
var f = args[++i].ToLower();
faction = f switch
{
"qrath" => DataType.FACTION_QRath,
"aru" => DataType.FACTION_Aru,
_ => throw new Exception($"Unknown faction '{args[i]}'. Use QRath or Aru.")
};
immortal = f switch
{
"qrath" => DataType.IMMORTAL_Orzum,
"aru" => DataType.IMMORTAL_Mala,
_ => immortal
};
break;
case "--immortal" when i + 1 < args.Length:
var im = args[++i];
immortal = im switch
{
nameof(DataType.IMMORTAL_Orzum) => DataType.IMMORTAL_Orzum,
nameof(DataType.IMMORTAL_Ajari) => DataType.IMMORTAL_Ajari,
nameof(DataType.IMMORTAL_Atzlan) => DataType.IMMORTAL_Atzlan,
nameof(DataType.IMMORTAL_Mala) => DataType.IMMORTAL_Mala,
nameof(DataType.IMMORTAL_Xol) => DataType.IMMORTAL_Xol,
_ => throw new Exception($"Unknown immortal '{im}'.")
};
break;
case "--attack-time" or "-a" when i + 1 < args.Length:
attackTime = int.Parse(args[++i]);
break;
default:
entityNames.Add(args[i]);
break;
}
var toastService = new ToastService();
var storageService = new NullStorageService();
var timingService = new TimingService(storageService);
timingService.SetAttackTime(attackTime);
var buildOrderService = new BuildOrderService(toastService, timingService);
var economyService = new EconomyService();
buildOrderService.Reset(faction);
economyService.Calculate(buildOrderService, timingService, 0);
Console.WriteLine($"Faction: {(faction == DataType.FACTION_QRath ? "Q'Rath" : "Aru")}");
Console.WriteLine($"Immortal: {immortal.Replace("IMMORTAL_", "")}");
Console.WriteLine($"Attack Time: {attackTime}s");
Console.WriteLine(new string('-', 50));
foreach (var name in entityNames)
{
if (name.StartsWith("wait ", StringComparison.OrdinalIgnoreCase))
{
var seconds = int.Parse(name[5..]);
buildOrderService.AddWait(seconds);
economyService.Calculate(buildOrderService, timingService, buildOrderService.GetLastRequestInterval());
Console.WriteLine($" Wait {seconds}s -> now at interval {buildOrderService.GetLastRequestInterval()}");
continue;
}
if (name.StartsWith("waitto ", StringComparison.OrdinalIgnoreCase))
{
var interval = int.Parse(name[7..]);
buildOrderService.AddWaitTo(interval);
economyService.Calculate(buildOrderService, timingService, buildOrderService.GetLastRequestInterval());
Console.WriteLine($" Wait to {interval}s -> now at interval {buildOrderService.GetLastRequestInterval()}");
continue;
}
var entity = FindEntity(name, faction, immortal);
if (entity == null)
{
Console.WriteLine($" ERROR: '{name}' not found for this faction/immortal.");
continue;
}
var beforeInterval = buildOrderService.GetLastRequestInterval();
var added = buildOrderService.Add(entity, economyService);
if (added)
{
economyService.Calculate(buildOrderService, timingService, buildOrderService.GetLastRequestInterval());
var startedAt = buildOrderService.GetLastRequestInterval();
var production = entity.Production();
var completedAt = production != null ? startedAt + production.BuildTime : startedAt;
var cost = production != null
? $" [{production.Alloy}a/{production.Ether}e/{production.Pyre}p, {production.BuildTime}s]"
: "";
Console.WriteLine($" {entity.GetName(),-25} start={startedAt,4}s done={completedAt,4}s{cost}");
}
else
{
Console.WriteLine($" ERROR: Could not add '{name}'.");
var toasts = toastService.GetToasts();
if (toasts.Count > 0)
{
var lastToast = toasts[0];
Console.WriteLine($" Reason: {lastToast.Title} - {lastToast.Message}");
}
}
}
Console.WriteLine(new string('-', 50));
var lastInterval = buildOrderService.GetLastRequestInterval();
var finalEconomy = economyService.GetEconomy(timingService.GetAttackTime());
var lastEconomy = economyService.GetEconomy(lastInterval);
Console.WriteLine($"Army Attacking At: {timingService.GetAttackTime()}s");
Console.WriteLine("");
Console.WriteLine($"At attack time ({timingService.GetAttackTime()}s):");
Console.WriteLine($" Alloy: {finalEconomy.Alloy,10:F1}");
Console.WriteLine($" Ether: {finalEconomy.Ether,10:F1}");
Console.WriteLine($" Pyre: {finalEconomy.Pyre,10:F1}");
Console.WriteLine("");
Console.WriteLine($"At last build action ({lastInterval}s):");
Console.WriteLine($" Alloy: {lastEconomy.Alloy,10:F1}");
Console.WriteLine($" Ether: {lastEconomy.Ether,10:F1}");
Console.WriteLine($" Pyre: {lastEconomy.Pyre,10:F1}");
static EntityModel? FindEntity(string name, string faction, string immortal)
{
var candidates = EntityModel.GetList()
.Where(e => e.Info()?.Name?.Equals(name, StringComparison.OrdinalIgnoreCase) == true)
.Where(e => e.Faction()?.Faction == faction)
.ToList();
if (candidates.Count == 0)
candidates = EntityModel.GetList()
.Where(e => e.Info()?.Name?.Equals(name, StringComparison.OrdinalIgnoreCase) == true)
.Where(e => e.Faction() == null || e.Faction()!.Faction == DataType.FACTION_Neutral)
.ToList();
if (candidates.Count == 0) return null;
if (candidates.Count == 1) return candidates[0];
var vanguardMatch = candidates.FirstOrDefault(e => e.VanguardAdded()?.ImmortalId == immortal);
if (vanguardMatch != null) return vanguardMatch;
return candidates.FirstOrDefault(e => e.VanguardAdded() == null);
}
+33
View File
@@ -0,0 +1,33 @@
using Services;
namespace IGP.Calculator.Cli.Services;
public class NullStorageService : IStorageService
{
private readonly Dictionary<string, object?> _store = new();
public void Subscribe(Action action)
{
}
public void Unsubscribe(Action action)
{
}
public T GetValue<T>(string forKey)
{
if (_store.TryGetValue(forKey, out var value) && value is T typed)
return typed;
return default!;
}
public void SetValue<T>(string key, T value)
{
_store[key] = value;
}
public Task Load()
{
return Task.CompletedTask;
}
}
+4 -3
View File
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Razor"> <Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Components</RootNamespace>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -29,8 +30,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Model\Model.csproj"/> <ProjectReference Include="..\Model\Model.csproj" />
<ProjectReference Include="..\Services\Services.csproj"/> <ProjectReference Include="..\Services\Services.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Inputs\"/> <None Remove="Inputs\"/>
+1 -1
View File
@@ -13,7 +13,7 @@
} }
<div> <div>
<input readonly="@MemoryQuestion.IsRevealed" <input readonly="@MemoryQuestion.IsRevealed"
class="formTextInput @(MemoryQuestion.IsRevealed ? "revealed" : IsSubmitted == false ? "guess" : int.Parse(guess ?? string.Empty) == MemoryQuestion.Answer ? "correct" : "wrong")" class="formTextInput @(MemoryQuestion.IsRevealed ? "revealed" : !IsSubmitted ? "guess" : int.Parse(guess ?? string.Empty) == MemoryQuestion.Answer ? "correct" : "wrong")"
placeholder="guess..." placeholder="guess..."
type="number" type="number"
value="@guess" value="@guess"
+9
View File
@@ -0,0 +1,9 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish Web/Web.csproj -c Release -o /app/publish
FROM nginx:alpine AS final
WORKDIR /usr/share/nginx/html
COPY --from=build /app/publish/wwwroot ./
COPY nginx.conf /etc/nginx/nginx.conf
+107
View File
@@ -0,0 +1,107 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Model\Model.csproj", "{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services", "Services\Services.csproj", "{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "Web\Web.csproj", "{4A54E237-4FF6-4459-91D9-9249AE3E72E0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "CLI\CLI.csproj", "{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components", "Components\Components.csproj", "{C0ACFFEB-8098-4119-9C28-3A4D671C2514}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|x64.ActiveCfg = Debug|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|x64.Build.0 = Debug|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|x86.ActiveCfg = Debug|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|x86.Build.0 = Debug|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|Any CPU.Build.0 = Release|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|x64.ActiveCfg = Release|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|x64.Build.0 = Release|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|x86.ActiveCfg = Release|Any CPU
{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|x86.Build.0 = Release|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|x64.ActiveCfg = Debug|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|x64.Build.0 = Debug|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|x86.ActiveCfg = Debug|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|x86.Build.0 = Debug|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|Any CPU.Build.0 = Release|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|x64.ActiveCfg = Release|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|x64.Build.0 = Release|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|x86.ActiveCfg = Release|Any CPU
{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|x86.Build.0 = Release|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|x64.ActiveCfg = Debug|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|x64.Build.0 = Debug|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|x86.ActiveCfg = Debug|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|x86.Build.0 = Debug|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|Any CPU.Build.0 = Release|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|x64.ActiveCfg = Release|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|x64.Build.0 = Release|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|x86.ActiveCfg = Release|Any CPU
{4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|x86.Build.0 = Release|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|x64.ActiveCfg = Debug|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|x64.Build.0 = Debug|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|x86.ActiveCfg = Debug|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|x86.Build.0 = Debug|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|Any CPU.Build.0 = Release|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|x64.ActiveCfg = Release|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|x64.Build.0 = Release|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|x86.ActiveCfg = Release|Any CPU
{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|x86.Build.0 = Release|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|x64.ActiveCfg = Debug|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|x64.Build.0 = Debug|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|x86.ActiveCfg = Debug|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|x86.Build.0 = Debug|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|Any CPU.Build.0 = Release|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|x64.ActiveCfg = Release|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|x64.Build.0 = Release|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|x86.ActiveCfg = Release|Any CPU
{C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|x86.Build.0 = Release|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|x64.ActiveCfg = Debug|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|x64.Build.0 = Debug|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|x86.ActiveCfg = Debug|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|x86.Build.0 = Debug|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|Any CPU.Build.0 = Release|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x64.ActiveCfg = Release|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x64.Build.0 = Release|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x86.ActiveCfg = Release|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {19100811-B909-4D20-9AE0-2EB579A55B3A}
EndGlobalSection
EndGlobal
-49
View File
@@ -1,49 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IGP", "IGP.csproj", "{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Model", "..\Model\Model.csproj", "{77395F7A-BE93-470C-9F10-F48FFA445B63}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components", "..\Components\Components.csproj", "{0419E7CD-0971-4A56-A61F-C090DF60FAF6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services", "..\Services\Services.csproj", "{621178C8-4E8B-478E-80E5-7478F0E7B67E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAutomation", "..\TestAutomation\TestAutomation.csproj", "{8B49D038-D013-460D-9C4F-817CAFFEB06F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|Any CPU.Build.0 = Release|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|Any CPU.Build.0 = Release|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|Any CPU.Build.0 = Release|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|Any CPU.Build.0 = Release|Any CPU
{8B49D038-D013-460D-9C4F-817CAFFEB06F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B49D038-D013-460D-9C4F-817CAFFEB06F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B49D038-D013-460D-9C4F-817CAFFEB06F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B49D038-D013-460D-9C4F-817CAFFEB06F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {19100811-B909-4D20-9AE0-2EB579A55B3A}
EndGlobalSection
EndGlobal
+4 -4
View File
@@ -48,7 +48,7 @@ public class BuildOrderModel
new List<EntityModel> new List<EntityModel>
{ {
EntityModel.Get(DataType.STARTING_Bastion), EntityModel.Get(DataType.STARTING_Bastion),
EntityModel.Get(DataType.STARTING_TownHall_Aru) EntityModel.Get(factionStartingTownHall)
} }
} }
}; };
@@ -59,7 +59,7 @@ public class BuildOrderModel
new List<EntityModel> new List<EntityModel>
{ {
EntityModel.Get(DataType.STARTING_Bastion), EntityModel.Get(DataType.STARTING_Bastion),
EntityModel.Get(DataType.STARTING_TownHall_Aru) EntityModel.Get(factionStartingTownHall)
} }
} }
}; };
@@ -69,7 +69,7 @@ public class BuildOrderModel
DataType.STARTING_Bastion, 0 DataType.STARTING_Bastion, 0
}, },
{ {
DataType.STARTING_TownHall_Aru, 0 factionStartingTownHall, 0
} }
}; };
UniqueCompletedCount = new Dictionary<string, int> UniqueCompletedCount = new Dictionary<string, int>
@@ -78,7 +78,7 @@ public class BuildOrderModel
DataType.STARTING_Bastion, 1 DataType.STARTING_Bastion, 1
}, },
{ {
DataType.STARTING_TownHall_Aru, 1 factionStartingTownHall, 1
} }
}; };
SupplyCountTimes = new Dictionary<int, int> SupplyCountTimes = new Dictionary<int, int>
+1 -2
View File
@@ -4,6 +4,5 @@ namespace Model.Entity.Parts;
public class IEntityPartInterface public class IEntityPartInterface
{ {
[JsonIgnore] [JsonIgnore] public EntityModel Parent { get; set; }
public EntityModel Parent { get; set; }
} }
+2 -1
View File
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<RootNamespace>Model</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1"/> <PackageReference Include="Newtonsoft.Json" Version="13.0.1"/>
-38
View File
@@ -1,38 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://calm-mud-04916b210.1.azurestaticapps.net/build-calculator', { timeout: 15000 });
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await page.locator('select').nth(0).selectOption("Q'Rath");
await page.waitForTimeout(300);
await page.locator('select').nth(1).selectOption('Orzum');
await page.waitForTimeout(1000);
const keys = await page.locator('.keyContainer > div > div').all();
for (const key of keys) {
const text = await key.textContent();
if (text && text.trim().startsWith('TAB')) {
console.log('TAB key textContent:', text.substring(0, 500));
const innerHtml = await key.evaluate(el => el.innerHTML);
console.log('TAB key innerHTML (first 2000):', innerHtml.substring(0, 2000));
const childDivs = await key.locator('> div').all();
console.log('Entity div count:', childDivs.length);
for (const div of childDivs) {
const t = await div.textContent();
const s = await div.getAttribute('style');
console.log(' Entity:', (t || '').trim(), 'Style:', (s || 'none').substring(0, 100));
}
}
}
await page.locator('.keyContainer > div > div').filter({ hasText: 'TAB' }).first().click();
await page.waitForTimeout(1000);
const entityViewName = await page.locator('.entityClickView #entityName').textContent();
console.log('Entity view shows:', entityViewName);
const entityViewHtml = await page.locator('.entityClickView').evaluate(el => el.innerHTML.substring(0, 1000));
console.log('Entity view HTML:', entityViewHtml);
await browser.close();
})().catch(e => { console.error(e.message); process.exit(1); });
-51
View File
@@ -1,51 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
page.on('console', msg => {
if (msg.type() === 'error' || msg.type() === 'warning')
console.log(msg.type().toUpperCase() + ':', msg.text().substring(0, 300));
});
page.on('pageerror', err => console.log('PAGE_ERR:', err.message));
await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'load' });
console.log('Page load event fired');
// Read button text immediately
let buttons = page.locator('.keyContainer > div > div');
let count = await buttons.count();
console.log('Button count:', count);
let qBtn = buttons.filter({ hasText: /^Q/ }).first();
console.log('Immediate Q button text:', JSON.stringify(await qBtn.textContent()));
// Wait for Blazor to finish initializing - look for .blazor-error-boundary or just wait
// Actually, let's poll for the button text changing from what it is now
for (let i = 0; i < 15; i++) {
await page.waitForTimeout(1000);
const text = (await buttons.nth(0).textContent() || '').trim();
// Check all 19 buttons for Q and F keys
const allTexts = [];
for (let j = 0; j < count; j++) {
const t = (await buttons.nth(j).textContent() || '').trim();
if (t.toUpperCase().startsWith('Q') || t.toUpperCase().startsWith('F') || t.toUpperCase().startsWith('W') || t.toUpperCase().startsWith('E')) {
allTexts.push(` B${j}: "${t.substring(0,30)}"`);
}
}
console.log(`After ${i+1}s:`);
allTexts.forEach(t => console.log(t));
}
// Now check what the filter is currently showing
const selects = page.locator('select');
console.log('\nSelect values:');
for (let s = 0; s < await selects.count(); s++) {
const val = await selects.nth(s).inputValue();
const opts = await selects.nth(s).locator('option').allTextContents();
const selectedIdx = await selects.nth(s).evaluate(el => el.selectedIndex);
console.log(` Select ${s}: value=${val}, selectedIndex=${selectedIdx}, options=${JSON.stringify(opts.map(o=>o.trim()))}`);
}
await browser.close();
})();
-74
View File
@@ -1,74 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
page.on('pageerror', err => console.log('PAGE_ERR:', err.message));
page.on('console', msg => {
if (msg.type() === 'error') console.log('CONSOLE_ERR:', msg.text().substring(0,200));
});
try {
await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'load' });
console.log('Page loaded');
} catch (e) {
console.log('Page load error:', e.message);
}
await page.waitForTimeout(10000);
console.log('Waited 10s');
const selCount = await page.locator('select').count();
console.log('Select elements count:', selCount);
if (selCount > 0) {
for (let s = 0; s < selCount; s++) {
const opts = await page.locator('select').nth(s).locator('option').allTextContents();
console.log('Select', s, 'options:', JSON.stringify(opts.map(o => o.trim())));
}
await page.locator('select').nth(0).selectOption("Q'Rath");
console.log('Selected Q\' Rath');
await page.waitForTimeout(500);
await page.locator('select').nth(1).selectOption('Orzum');
console.log('Selected Orzum');
await page.waitForTimeout(2000);
// Log all key buttons
const buttons = page.locator('.keyContainer > div > div');
const count = await buttons.count();
console.log('=== All key buttons (' + count + ') ===');
for (let i = 0; i < count; i++) {
const txt = (await buttons.nth(i).textContent() || '').trim();
console.log('Button', i, ':', JSON.stringify(txt.substring(0,60)));
}
// Find Q button
for (let i = 0; i < count; i++) {
const txt = (await buttons.nth(i).textContent() || '');
if (txt.trim().toUpperCase().startsWith('Q')) {
console.log('\nFound Q button at index', i, ':', JSON.stringify(txt.trim().substring(0,60)));
console.log('Clicking it...');
await buttons.nth(i).click({ force: true });
break;
}
}
await page.waitForTimeout(1000);
// Check entity view
const evCount = await page.locator('.entityClickView #entityName').count();
console.log('entityName count:', evCount);
if (evCount > 0) {
const name = (await page.locator('.entityClickView #entityName').textContent() || '').trim();
console.log('entityName text:', JSON.stringify(name));
}
} else {
console.log('No select elements found!');
console.log('URL:', page.url());
const body = await page.evaluate(() => document.body.innerText.substring(0, 500));
console.log('Body text:', JSON.stringify(body));
}
await browser.close();
})();
-59
View File
@@ -1,59 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
console.log('Navigating...');
await page.goto('https://calm-mud-04916b210.1.azurestaticapps.net/build-calculator', { timeout: 30000, waitUntil: 'domcontentloaded' });
console.log('Page loaded, waiting for Blazor...');
await page.waitForTimeout(8000);
console.log('Selecting Q\'Rath...');
await page.locator('select').nth(0).selectOption("Q'Rath");
await page.waitForTimeout(500);
await page.locator('select').nth(1).selectOption('Orzum');
await page.waitForTimeout(3000);
console.log('Looking for TAB key...');
const allKeys = await page.locator('.keyContainer > div > div').all();
console.log('Total key divs found:', allKeys.length);
for (let i = 0; i < allKeys.length; i++) {
const text = await allKeys[i].textContent();
const preview = (text || '').trim().substring(0, 100);
console.log('Key ' + i + ' starts with:', preview.replace(/\n/g, ' '));
if (text && text.trim().startsWith('TAB')) {
console.log('FOUND TAB KEY at index ' + i);
const entityDivs = await allKeys[i].locator('> div').all();
console.log('Entity divs:', entityDivs.length);
for (const d of entityDivs) {
const t = await d.textContent();
console.log(' Entity:', (t || '').trim());
}
}
}
console.log('Clicking TAB key...');
try {
const tabKey = page.locator('.keyContainer > div > div').filter({ hasText: 'TAB' }).first();
await tabKey.click({ timeout: 5000 });
console.log('Click succeeded');
} catch (e) {
console.log('Click failed:', e.message.substring(0, 200));
}
await page.waitForTimeout(3000);
const entityViewCount = await page.locator('.entityClickView').count();
console.log('entityClickView count:', entityViewCount);
if (entityViewCount > 0) {
const ev = await page.locator('.entityClickView #entityName').textContent();
console.log('Entity view name:', ev);
const id = await page.locator('.entityClickView .entitiesContainer').getAttribute('id');
console.log('Entity container id:', id);
} else {
console.log('Entity click view NOT found - checking for errors...');
const errorEls = await page.locator('[class*=\"error\"], [class*=\"Error\"]').count();
console.log('Error elements:', errorEls);
}
await browser.close();
})().catch(e => { console.error(e.message); process.exit(1); });
-69
View File
@@ -1,69 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'networkidle' });
await page.waitForTimeout(10000);
const selects = page.locator('select');
await selects.nth(0).selectOption("Q'Rath");
await page.waitForTimeout(500);
await selects.nth(1).selectOption('Orzum');
await page.waitForTimeout(2000);
// Check entity view before adding
let evCount = await page.locator('.entityClickView #entityName').count();
console.log('entityName count before add:', evCount);
if (evCount > 0) console.log('entityName text:', (await page.locator('.entityClickView #entityName').textContent() || '').trim());
// Check timeline intervals before
let intervals = page.locator('[class*="interval"], .timelineInterval');
console.log('interval count before add:', await intervals.count());
if ((await intervals.count()) > 0) {
console.log('first interval text:', ((await intervals.first().textContent()) || '').trim().substring(0,100));
}
// Click Q
const buttons = page.locator('.keyContainer > div > div');
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const txt = (await buttons.nth(i).textContent()) || '';
if (txt.trim().toUpperCase().startsWith('Q')) {
await buttons.nth(i).click({ force: true });
console.log('Clicked Q button at index', i);
break;
}
}
await page.waitForTimeout(2000);
// Check entity view after adding
evCount = await page.locator('.entityClickView #entityName').count();
console.log('entityName count after add:', evCount);
if (evCount > 0) console.log('entityName text:', ((await page.locator('.entityClickView #entityName').textContent()) || '').trim());
// Check timeline intervals after
intervals = page.locator('[class*="interval"], .timelineInterval');
console.log('interval count after add:', await intervals.count());
if ((await intervals.count()) > 0) {
const ic = await intervals.count();
for (let i = 0; i < Math.min(ic, 3); i++) {
console.log('interval', i, 'text:', ((await intervals.nth(i).textContent()) || '').trim().substring(0,120));
}
}
// Click Clear Build Order
await page.locator('button').filter({ hasText: 'Clear Build Order' }).click();
await page.waitForTimeout(2000);
// Check entity view after clear
evCount = await page.locator('.entityClickView #entityName').count();
console.log('entityName count after clear:', evCount);
if (evCount > 0) console.log('entityName text:', ((await page.locator('.entityClickView #entityName').textContent()) || '').trim());
// Check timeline intervals after clear
intervals = page.locator('[class*="interval"], .timelineInterval');
console.log('interval count after clear:', await intervals.count());
await browser.close();
})();
-39
View File
@@ -1,39 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'networkidle' });
await page.waitForTimeout(10000);
const selects = page.locator('select');
await selects.nth(0).selectOption("Q'Rath");
await page.waitForTimeout(500);
await selects.nth(1).selectOption('Orzum');
await page.waitForTimeout(2000);
const grid = page.locator('.calculatorGrid > div');
const gCount = await grid.count();
console.log('calculatorGrid child divs:', gCount);
for (let i = 0; i < gCount; i++) {
const cls = await grid.nth(i).getAttribute('class');
const text = (await grid.nth(i).textContent() || '').trim().substring(0,80);
console.log(' child', i, 'class:', JSON.stringify(cls), 'text:', JSON.stringify(text));
}
// Check for interval-related elements
for (const sel of ['[class*="interval"]', '[class*="Interval"]', '[class*="timeline"]', '[class*="Timeline"]']) {
console.log(sel, 'count:', await page.locator(sel).count());
}
// Also check displayContainer children
const dc = page.locator('.displayContainer');
const dcCount = await dc.count();
console.log('displayContainer count:', dcCount);
for (let i = 0; i < dcCount; i++) {
const cls = await dc.nth(i).getAttribute('class');
const text = (await dc.nth(i).textContent() || '').trim().substring(0,150);
console.log(' dc', i, 'class:', JSON.stringify(cls), 'text:', JSON.stringify(text));
}
await browser.close();
})();
-90
View File
@@ -1,90 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'networkidle' });
await page.waitForTimeout(10000);
const selects = page.locator('select');
await selects.nth(0).selectOption("Q'Rath");
await page.waitForTimeout(500);
await selects.nth(1).selectOption('Orzum');
await page.waitForTimeout(2000);
const gridItems = page.locator('.calculatorGrid > div');
const gCount = await gridItems.count();
// Find the timeline section (has "Shows economy" text)
let timelineIdx = -1;
for (let i = 0; i < gCount; i++) {
const txt = (await gridItems.nth(i).textContent() || '');
if (txt.includes('economy')) {
timelineIdx = i;
break;
}
}
console.log('Timeline grid item index:', timelineIdx);
// Get full timeline text before adding
if (timelineIdx >= 0) {
const text = (await gridItems.nth(timelineIdx).textContent() || '').trim();
console.log('Timeline text before add (first 500 chars):');
console.log(text.substring(0, 500));
console.log('...');
console.log('Contains Acropolis:', text.includes('Acropolis'));
console.log('Contains Requested:', text.includes('Requested'));
}
// Click Q to add Acropolis
const buttons = page.locator('.keyContainer > div > div');
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const txt = (await buttons.nth(i).textContent()) || '';
if (txt.trim().toUpperCase().startsWith('Q')) {
await buttons.nth(i).click({ force: true });
console.log('Clicked Q at index', i);
break;
}
}
await page.waitForTimeout(2000);
// Check entity view
const evName = (await page.locator('.entityClickView #entityName').textContent() || '').trim();
console.log('EntityView name after add:', evName);
// Get timeline text after adding
if (timelineIdx >= 0) {
const text = (await gridItems.nth(timelineIdx).textContent() || '').trim();
console.log('Timeline text after add (first 500 chars):');
console.log(text.substring(0, 500));
console.log('Contains Acropolis:', text.includes('Acropolis'));
console.log('Contains Requested:', text.includes('Requested'));
console.log('Contains New:', text.includes('New'));
}
// Count Virtualize items in timeline
const virtualItems = page.locator('[style*="grid-template-columns: 1fr 1fr"]');
console.log('Virtualize items (grid items):', await virtualItems.count());
// Click Clear Build Order
await page.locator('button').filter({ hasText: 'Clear Build Order' }).click();
await page.waitForTimeout(2000);
// Check entity view after clear
const evAfterClear = await page.locator('.entityClickView #entityName').count();
console.log('EntityView #entityName count after clear:', evAfterClear);
if (evAfterClear > 0) {
console.log('EntityView name after clear:', (await page.locator('.entityClickView #entityName').textContent() || '').trim());
}
// Check timeline after clear
if (timelineIdx >= 0) {
const text = (await gridItems.nth(timelineIdx).textContent() || '').trim();
console.log('Timeline text after clear (first 300 chars):');
console.log(text.substring(0, 300));
console.log('Contains Acropolis:', text.includes('Acropolis'));
console.log('Virtualize items:', await virtualItems.count());
}
await browser.close();
})();
-64
View File
@@ -1,64 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'networkidle' });
await page.waitForTimeout(10000);
const selects = page.locator('select');
await selects.nth(0).selectOption("Q'Rath");
await page.waitForTimeout(500);
await selects.nth(1).selectOption('Orzum');
await page.waitForTimeout(2000);
const gridItems = page.locator('.calculatorGrid > div');
const gCount = await gridItems.count();
for (let i = 0; i < gCount; i++) {
const txt = (await gridItems.nth(i).textContent() || '').trim();
const cls = await gridItems.nth(i).getAttribute('class');
console.log('=== Grid Item', i, 'class:', cls, '===');
console.log(txt.substring(0, 200));
console.log('---');
}
console.log('\n=== Clicking Q ===');
const buttons = page.locator('.keyContainer > div > div');
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const txt = (await buttons.nth(i).textContent()) || '';
if (txt.trim().toUpperCase().startsWith('Q')) {
await buttons.nth(i).click({ force: true });
break;
}
}
await page.waitForTimeout(2000);
console.log('\n=== After Q click ===');
for (let i = 0; i < gCount; i++) {
const txt = (await gridItems.nth(i).textContent() || '').trim();
console.log('Grid', i, '- contains Acropolis:', txt.includes('Acropolis'));
if (txt.includes('Acropolis')) {
console.log(' Text around Acropolis:');
const idx = txt.indexOf('Acropolis');
console.log(' ', txt.substring(Math.max(0, idx - 40), idx + 40));
}
}
// Entity view
console.log('\nEntityView name:', (await page.locator('.entityClickView #entityName').textContent() || '').trim());
// Click Clear
await page.locator('button').filter({ hasText: 'Clear Build Order' }).click();
await page.waitForTimeout(2000);
console.log('\n=== After Clear ===');
for (let i = 0; i < gCount; i++) {
const txt = (await gridItems.nth(i).textContent() || '').trim();
console.log('Grid', i, '- contains Acropolis:', txt.includes('Acropolis'));
}
console.log('\nEntityView after clear count:', await page.locator('.entityClickView #entityName').count());
await browser.close();
})();
+1 -3
View File
@@ -11,9 +11,7 @@ class Website {
} else { } else {
const hook = process.env.TEST_HOOK || ''; const hook = process.env.TEST_HOOK || '';
this.deploymentType = hook.includes('localhost') ? 'Local' : 'Dev'; this.deploymentType = hook.includes('localhost') ? 'Local' : 'Dev';
this.baseUrl = this.deploymentType === 'Dev' this.baseUrl = 'https://localhost:7234';
? 'https://calm-mud-04916b210.1.azurestaticapps.net'
: 'https://localhost:7234';
} }
const BuildCalculatorPage = require('../pages/buildCalculatorPage'); const BuildCalculatorPage = require('../pages/buildCalculatorPage');
-21
View File
@@ -58,33 +58,12 @@ test.describe('Build Calculator', () => {
const calc = website.buildCalculatorPage; const calc = website.buildCalculatorPage;
await calc.goto(); await calc.goto();
const buttons = page.locator('.keyContainer > div > div');
console.log('Initial Q button text:', await buttons.filter({ hasText: /^Q/ }).first().textContent());
// Wait for Blazor re-render to complete by waiting for the button text to stabilize
// (it goes from QAcropolis → empty during re-render → back to QAcropolis)
let tries = 0;
let text = '';
while (tries < 20) {
await page.waitForTimeout(500);
try {
text = (await buttons.filter({ hasText: /^Q/ }).first().textContent() || '').trim();
if (text && text.length > 1) break;
} catch { }
tries++;
}
console.log(`After Blazor render (${(tries+1)*0.5}s): Q button text: ${JSON.stringify(text)}`);
await calc.filter.selectFaction("Q'Rath"); await calc.filter.selectFaction("Q'Rath");
await calc.filter.selectImmortal('Orzum'); await calc.filter.selectImmortal('Orzum');
await page.waitForTimeout(1000);
console.log('After filter Q button text:', await buttons.filter({ hasText: /^Q/ }).first().textContent());
expect(await calc.timeline.containsEntity('Acropolis')).toBe(false); expect(await calc.timeline.containsEntity('Acropolis')).toBe(false);
await calc.hotkeys.clickKey('Q'); await calc.hotkeys.clickKey('Q');
await page.waitForTimeout(1000);
expect(await calc.entityView.getEntityName()).toBe('Acropolis'); expect(await calc.entityView.getEntityName()).toBe('Acropolis');
expect(await calc.timeline.containsEntity('Acropolis')).toBe(true); expect(await calc.timeline.containsEntity('Acropolis')).toBe(true);
+1 -1
View File
@@ -114,7 +114,6 @@ public interface IEntityDialogService
public bool HasHistory(); public bool HasHistory();
} }
public interface INoteService public interface INoteService
{ {
public List<NoteContentModel> NoteContentModels { get; set; } public List<NoteContentModel> NoteContentModels { get; set; }
@@ -291,6 +290,7 @@ public interface IBuildOrderService
public void RemoveLast(); public void RemoveLast();
public void Reset(); public void Reset();
public void Reset(string faction);
public int GetLastRequestInterval(); public int GetLastRequestInterval();
public string BuildOrderAsYaml(); public string BuildOrderAsYaml();
+7
View File
@@ -341,6 +341,13 @@ public class BuildOrderService : IBuildOrderService
NotifyDataChanged(); NotifyDataChanged();
} }
public void Reset(string faction)
{
_lastInterval = 0;
_buildOrder.Initialize(faction);
NotifyDataChanged();
}
public int? WillMeetTrainingQueue(EntityModel entity) public int? WillMeetTrainingQueue(EntityModel entity)
{ {
var supply = entity.Supply(); var supply = entity.Supply();
@@ -192,7 +192,7 @@ public class EconomyComparisionService : IEconomyComparisonService
if (usedWorkers < harvester.Slots) workersNeeded += 1; if (usedWorkers < harvester.Slots) workersNeeded += 1;
} }
if (harvester.RequiresWorker == false) if (!harvester.RequiresWorker)
{ {
if (harvester.Resource == ResourceType.Ether) if (harvester.Resource == ResourceType.Ether)
economyAtSecond.Ether += harvester.HarvestedPerInterval * harvester.Slots; economyAtSecond.Ether += harvester.HarvestedPerInterval * harvester.Slots;
+3 -2
View File
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>Services</RootNamespace>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -22,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Model\Model.csproj"/> <ProjectReference Include="..\Model\Model.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
-43
View File
@@ -1,43 +0,0 @@
using TestAutomation.Utils;
namespace TestAutomation;
public enum DeploymentType
{
Dev,
Local
}
public class BaseTest
{
protected static readonly TestReport TestReport = new();
protected static Website WebsiteInstance = default!;
protected readonly HttpClient HttpClient = new();
protected static Website Website
{
get
{
if (WebsiteInstance == null)
{
var options = new FirefoxOptions();
options.AcceptInsecureCertificates = true;
if (Website.DeploymentType.Equals(DeploymentType.Dev)) options.AddArgument("--headless");
options.AddArgument("--ignore-certificate-errors");
options.AddArgument("--start-maximized");
options.AddArgument("--test-type");
options.AddArgument("--allow-running-insecure-content");
IWebDriver webDriver = new FirefoxDriver(Environment.CurrentDirectory, options);
WebsiteInstance = new Website(webDriver, TestReport);
}
return WebsiteInstance;
}
}
}
-8
View File
@@ -1,8 +0,0 @@
namespace TestAutomation.Enums;
public enum ScreenType
{
Desktop,
Tablet,
Mobile
}
-29
View File
@@ -1,29 +0,0 @@
using TestAutomation.Shared;
using TestAutomation.Utils;
namespace TestAutomation.Pages;
public abstract class BasePage : BaseElement
{
protected BasePage(Website website) : base(website)
{
}
private IEnumerable<string> Links =>
Website.FindAllWithTag(Website.Find("content"), "a")
.Select(x => x.GetAttribute("href"));
public abstract string Url { get; set; }
public IEnumerable<string> GetLinks()
{
try
{
return Links;
}
catch (Exception e)
{
throw new Exception($"Couldn't get links on page {Url}");
}
}
}
-53
View File
@@ -1,53 +0,0 @@
using System.Collections.ObjectModel;
using TestAutomation.Utils;
namespace TestAutomation.Pages;
public class DatabasePage : BasePage
{
public DatabasePage(Website website) : base(website)
{
}
private IWebElement FilterNameInput => Website.Find("filterName");
public override string Url { get; set; } = "database";
private ReadOnlyCollection<IWebElement> EntityNames()
{
return Website.FindAll("entityName");
}
private IWebElement EntityName(string entityType, string entityName)
{
return Website.Find("entityName",
$"{entityType.ToLower()}-{entityName.ToLower()}");
}
public DatabasePage FilterName(string name)
{
Website.EnterInput(FilterNameInput, name);
return this;
}
public DatabasePage GetEntityName(string entityType, string entityName, out string result)
{
result = EntityName(entityType, entityName).Text;
return this;
}
public DatabasePage GetEntityName(int index, out string result)
{
result = EntityNames()[index].Text;
return this;
}
public DatabasePage Goto()
{
Website.Goto(Url);
return this;
}
}
@@ -1,49 +0,0 @@
using TestAutomation.Utils;
namespace TestAutomation.Pages;
public class DatabaseSinglePage : BasePage
{
public DatabaseSinglePage(Website website) : base(website)
{
}
private IWebElement EntityName => Website.Find("entityName");
private IWebElement EntityHealth => Website.Find("entityHealth");
private IWebElement InvalidSearch => Website.Find("invalidSearch");
private IWebElement ValidSearch => Website.Find("validSearch");
public override string Url { get; set; } = "database";
public DatabaseSinglePage GetEntityName(out string result)
{
result = EntityName.Text;
return this;
}
public DatabaseSinglePage GetEntityHealth(out string result)
{
result = EntityHealth.Text;
return this;
}
public DatabaseSinglePage GetInvalidSearch(out string result)
{
result = InvalidSearch.Text;
return this;
}
public DatabaseSinglePage GetValidSearch(out string result)
{
result = ValidSearch.Text;
return this;
}
public DatabaseSinglePage Goto(string searchText)
{
Website.Goto($"{Url}/{searchText}");
return this;
}
}
@@ -1,99 +0,0 @@
using TestAutomation.Utils;
namespace TestAutomation.Pages;
public class HarassCalculatorPage : BasePage
{
public HarassCalculatorPage(Website website) : base(website)
{
}
private IWebElement NumberOfWorkersLostToHarass => Website.Find("numberOfWorkersLostToHarass");
private IWebElement NumberOfTownHallsExisting => Website.Find("numberOfTownHallsExisting");
private IList<IWebElement> OnTownHallTravelTimes => Website.FindChildren("numberOfTownHallTravelTimes", "input");
private int TotalAlloyHarassment => Website.FindInt("totalAlloyHarassment");
private int WorkerReplacementCost => Website.FindInt("workerReplacementCost");
private int DelayedMiningCost => Website.FindInt("delayedMiningCost");
private int AverageTravelTime => Website.FindInt("getAverageTravelTime");
private int ExampleTotalAlloyLoss => Website.FindInt("exampleTotalAlloyLoss");
private int ExampleWorkerCost => Website.FindInt("exampleWorkerCost");
private int ExampleMiningTimeCost => Website.FindInt("exampleMiningTimeCost");
private int ExampleTotalAlloyLossDifference => Website.FindInt("exampleTotalAlloyLossDifference");
private int ExampleTotalAlloyLossAccurate => Website.FindInt("exampleTotalAlloyLossAccurate");
private int ExampleTotalAlloyLossAccurateDifference => Website.FindInt("exampleTotalAlloyLossAccurateDifference");
public override string Url { get; set; } = "harass-calculator";
public HarassCalculatorPage SetWorkersLostToHarass(int number)
{
Website.EnterInput(NumberOfWorkersLostToHarass, number);
return this;
}
public HarassCalculatorPage SetNumberOfTownHallsExisting(int number)
{
Website.EnterInput(NumberOfTownHallsExisting, number);
return this;
}
public HarassCalculatorPage SetTownHallTravelTime(int forTownHall, int number)
{
Website.EnterInput(OnTownHallTravelTimes[forTownHall], number);
return this;
}
public HarassCalculatorPage GetTotalAlloyHarassment(out int result)
{
result = TotalAlloyHarassment;
return this;
}
public HarassCalculatorPage GetExampleTotalAlloyLoss(out int result)
{
result = ExampleTotalAlloyLoss;
return this;
}
public HarassCalculatorPage GetExampleWorkerCost(out int result)
{
result = ExampleWorkerCost;
return this;
}
public HarassCalculatorPage GetExampleMiningTimeCost(out int result)
{
result = ExampleMiningTimeCost;
return this;
}
public HarassCalculatorPage GetExampleTotalAlloyLossAccurate(out int result)
{
result = ExampleTotalAlloyLossAccurate;
return this;
}
public HarassCalculatorPage GetExampleTotalAlloyLossDifference(out int result)
{
result = ExampleTotalAlloyLossDifference;
return this;
}
public HarassCalculatorPage GetExampleTotalAlloyLossAccurateDifference(out int result)
{
result = ExampleTotalAlloyLossAccurateDifference;
return this;
}
protected HarassCalculatorPage NavigateTo()
{
return this;
}
public HarassCalculatorPage Goto()
{
Website.Goto(Url);
return this;
}
}
-13
View File
@@ -1,13 +0,0 @@
using TestAutomation.Utils;
namespace TestAutomation.Shared;
public abstract class BaseElement
{
protected readonly Website Website;
protected BaseElement(Website website)
{
Website = website;
}
}
-25
View File
@@ -1,25 +0,0 @@
using TestAutomation.Utils;
namespace TestAutomation.Shared;
public class NavigationBar : BaseElement
{
public NavigationBar(Website website) : base(website)
{
}
private IWebElement HomeLink => Website.FindScreenSpecific("homeLink");
private IWebElement SearchButton => Website.FindScreenSpecific("searchButton");
public NavigationBar ClickHomeLink()
{
Website.Click(HomeLink);
return this;
}
public WebsiteSearchDialog ClickSearchButton()
{
Website.Click(SearchButton);
return Website.WebsiteSearchDialog;
}
}
@@ -1,32 +0,0 @@
using TestAutomation.Utils;
namespace TestAutomation.Shared;
public class WebsiteSearchDialog : BaseElement
{
public WebsiteSearchDialog(Website website) : base(website)
{
}
public IWebElement SearchBackground => Website.Find("searchBackground");
public IWebElement SearchInput => Website.Find("searchInput");
public NavigationBar CloseDialog()
{
Website.ClickTopLeft();
return Website.NavigationBar;
}
public WebsiteSearchDialog Search(string throne)
{
Website.EnterInput(SearchInput, throne);
return this;
}
public DatabaseSinglePage SelectSearchEntity(string throne)
{
Website.Click(Website.FindButtonWithLabel(throne));
return Website.DatabaseSinglePage;
}
}
-28
View File
@@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net.Webhook" Version="3.6.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.14"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0"/>
<PackageReference Include="NUnit" Version="3.13.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"/>
<PackageReference Include="NUnit.Analyzers" Version="3.2.0"/>
<PackageReference Include="coverlet.collector" Version="3.1.0"/>
<PackageReference Include="Selenium.WebDriver" Version="4.1.0"/>
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="101.0.4951.4100"/>
<PackageReference Include="Selenium.WebDriver.GeckoDriver" Version="0.31.0"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Pages\"/>
</ItemGroup>
</Project>
-95
View File
@@ -1,95 +0,0 @@
using TestAutomation.Utils;
namespace TestAutomation;
[TestFixture]
public class TestHarassCalculator : BaseTest
{
[SetUp]
public void SetUp()
{
TestReport.CreateTest();
}
[TearDown]
public void TearDown()
{
TestReport.ThrowErrors();
}
[Test]
public void CalculatorInput()
{
var expectedTotalAlloyHarassment = 240;
Website.HarassCalculatorPage
.Goto()
.SetWorkersLostToHarass(3)
.SetNumberOfTownHallsExisting(2)
.SetTownHallTravelTime(0, 30)
.GetTotalAlloyHarassment(out var foundTotalAlloyHarassment);
TestReport.CheckPassed(expectedTotalAlloyHarassment.Equals(foundTotalAlloyHarassment),
TestMessage.CreateFailedMessage($"expectTotalAlloyHarassment of {expectedTotalAlloyHarassment} " +
"does not equal " +
$"foundTotalAlloyHarassment of {foundTotalAlloyHarassment} "));
}
[Test]
public void CalculatedExampleInformation()
{
var expectedExampleTotalAlloyLoss = 720;
var expectedExampleWorkerCost = 300;
var expectedExampleMiningTimeCost = 420;
var expectedExampleTotalAlloyLossDifference = 300;
var expectedExampleTotalAlloyLossAccurate = 450;
var expectedExampleTotalAlloyLossAccurateDifference = 270;
Website.HarassCalculatorPage
.Goto()
.GetExampleTotalAlloyLoss(out var foundTotalAlloyLoss)
.GetExampleWorkerCost(out var foundExampleWorkerCost)
.GetExampleMiningTimeCost(out var foundExampleMiningTimeCost)
.GetExampleTotalAlloyLossAccurate(out var foundExampleTotalAlloyLossAccurate)
.GetExampleTotalAlloyLossDifference(out var foundGetExampleTotalAlloyLossDifference)
.GetExampleTotalAlloyLossAccurateDifference(out var foundExampleTotalAlloyLossAccurateDifference);
TestReport.CheckPassed(expectedExampleTotalAlloyLoss.Equals(foundTotalAlloyLoss),
TestMessage.CreateFailedMessage($"expectedExampleTotalAlloyLoss of {expectedExampleTotalAlloyLoss} " +
"does not equal " +
$"foundTotalAlloyLoss of {foundTotalAlloyLoss} "));
TestReport.CheckPassed(expectedExampleWorkerCost.Equals(foundExampleWorkerCost),
TestMessage.CreateFailedMessage($"expectedExampleWorkerCost of {expectedExampleWorkerCost} " +
"does not equal " +
$"foundExampleWorkerCost of {foundExampleWorkerCost} "));
TestReport.CheckPassed(expectedExampleMiningTimeCost.Equals(foundExampleMiningTimeCost),
TestMessage.CreateFailedMessage($"expectedExampleMiningTimeCost of {expectedExampleMiningTimeCost} " +
"does not equal " +
$"foundExampleMiningTimeCost of {foundExampleMiningTimeCost} "));
TestReport.CheckPassed(expectedExampleTotalAlloyLossAccurate.Equals(foundExampleTotalAlloyLossAccurate),
TestMessage.CreateFailedMessage(
$"expectedExampleTotalAlloyLossAccurate of {expectedExampleTotalAlloyLossAccurate} " +
"does not equal " +
$"foundExampleTotalAlloyLossAccurate of {foundExampleTotalAlloyLossAccurate} "));
TestReport.CheckPassed(expectedExampleTotalAlloyLossDifference.Equals(foundGetExampleTotalAlloyLossDifference),
TestMessage.CreateFailedMessage(
$"expectedExampleTotalAlloyLossDifference of {expectedExampleTotalAlloyLossDifference} " +
"does not equal " +
$"foundGetExampleTotalAlloyLossDifference of {foundGetExampleTotalAlloyLossDifference} "));
TestReport.CheckPassed(
expectedExampleTotalAlloyLossAccurateDifference.Equals(foundExampleTotalAlloyLossAccurateDifference),
TestMessage.CreateFailedMessage(
$"expectedExampleTotalAlloyLossAccurateDifference of {expectedExampleTotalAlloyLossAccurateDifference} " +
"does not equal " +
$"foundExampleTotalAlloyLossAccurateDifference of {foundExampleTotalAlloyLossAccurateDifference} "));
}
}
-30
View File
@@ -1,30 +0,0 @@
namespace TestAutomation;
[TestFixture]
public class TestLinks : BaseTest
{
[SetUp]
public void SetUp()
{
TestReport.CreateTest();
}
[TearDown]
public void TearDown()
{
TestReport.ThrowErrors();
}
[Test]
public void VerifyPageLinks()
{
Website.HarassCalculatorPage.Goto();
TestReport.VerifyLinks(Website.HarassCalculatorPage).Wait();
Website.DatabasePage.Goto();
TestReport.VerifyLinks(Website.DatabasePage).Wait();
Website.DatabaseSinglePage.Goto("throne");
TestReport.VerifyLinks(Website.DatabaseSinglePage).Wait();
}
}
-84
View File
@@ -1,84 +0,0 @@
using TestAutomation.Utils;
namespace TestAutomation;
[TestFixture]
public class TestSearchFeatures : BaseTest
{
[SetUp]
public void SetUp()
{
TestReport.CreateTest();
}
[TearDown]
public void TearDown()
{
TestReport.ThrowErrors();
}
[Test]
public void DesktopOpenCloseSearchDialog()
{
Website
.Goto()
.NavigationBar
.ClickSearchButton()
.CloseDialog()
.ClickHomeLink();
}
[Test]
public void DesktopSearchForThrone()
{
Website
.Goto()
.NavigationBar.ClickSearchButton()
.Search("Throne")
.SelectSearchEntity("Throne")
.GetEntityName(out var name)
.GetEntityHealth(out var health);
TestReport.CheckPassed(name.Equals("Throne"),
new TestMessage { Description = "Couldn't find Throne via search." });
TestReport.CheckPassed(!health.Trim().Equals(""),
new TestMessage { Description = "Throne has no visible health!" });
}
[Test]
public void DesktopFilterForThrone()
{
Website.DatabasePage
.Goto()
.FilterName("Throne")
.GetEntityName(0, out var name);
TestReport.CheckPassed(name.Equals("Throne"),
new TestMessage { Description = "Couldn't find Throne via filter." });
}
[Test]
public void SeeThroneByDefault()
{
Website.DatabasePage
.Goto()
.GetEntityName("army", "throne", out var name);
TestReport.CheckPassed(name.Equals("Throne"),
new TestMessage { Description = "Couldn't find Throne on the page by default." });
}
[Test]
public void DirectLinkNotThroneFailure()
{
Website.DatabaseSinglePage
.Goto("not throne")
.GetInvalidSearch(out var invalidSearch)
.GetValidSearch(out var validSearch);
TestReport.CheckPassed(invalidSearch.Equals("not throne"),
new TestMessage { Description = "Couldn't find invalid search text on the page." });
TestReport.CheckPassed(validSearch.Equals("Throne"),
new TestMessage { Description = "Couldn't find valid search text on the page." });
}
}
-8
View File
@@ -1,8 +0,0 @@
namespace TestAutomation.Utils;
public class Test
{
public string Name { get; set; } = "Name...";
public bool Result { get; set; } = true;
public IList<TestMessage> Messages { get; set; } = new List<TestMessage>();
}
-13
View File
@@ -1,13 +0,0 @@
namespace TestAutomation.Utils;
public class TestMessage
{
public string Title { get; set; } = "Name...";
public string Description { get; set; } = "";
public string Color { get; set; } = "FFFFFF";
public static TestMessage CreateFailedMessage(string description)
{
return new TestMessage { Title = "Check Failed", Description = description, Color = "FF0000" };
}
}
-111
View File
@@ -1,111 +0,0 @@
using System.Globalization;
using System.Runtime.CompilerServices;
namespace TestAutomation.Utils;
public class TestReport
{
private List<Test> Tests { get; } = new();
[MethodImpl(MethodImplOptions.NoInlining)]
public Test CreateTest()
{
Tests.Add(new Test
{
Name = TestContext.CurrentContext.Test.Name
});
return Tests.Last();
}
public void ThrowErrors()
{
if (!Tests.Last().Result)
{
var messages = string.Join("\n", Tests.Last().Messages.Select(x => x.Description).ToList());
throw new Exception(
$"{Tests.Last().Name} test failed with {Tests.Last().Messages.Count} messages.\n\n{messages}");
}
}
public async Task VerifyLinks(BasePage page)
{
foreach (var link in page.GetLinks())
try
{
if (link.StartsWith("mailto")) continue;
using var client = new HttpClient();
var response = await client.GetAsync(link);
if (!response.IsSuccessStatusCode)
{
CheckPassed(false,
new TestMessage
{
Color = "red", Title = "Bad Link",
Description = $"{link} failed on page {page.Url} with status code {response.StatusCode}"
});
Console.WriteLine(response.StatusCode.ToString());
}
}
catch (Exception e)
{
CheckPassed(false,
new TestMessage
{
Color = "red", Title = "Bad Link",
Description = $"{link} failed on page {page.Url} with stacktrace {e.StackTrace}"
});
}
}
public void CheckPassed(bool passed, TestMessage message)
{
if (passed) return;
Tests.Last().Result = false;
Tests.Last().Messages.Add(message);
}
public bool DidTestsPass()
{
foreach (var test in Tests)
{
if (test.Result) continue;
return false;
}
return true;
}
public List<object> GetMessages()
{
if (DidTestsPass())
return new List<object>
{
new
{
title = "Passed",
color = int.Parse("00FF00", NumberStyles.HexNumber),
description = $"All {Tests.Count} tests passed."
}
};
var messageList = new List<object>();
foreach (var test in Tests)
foreach (var message in test.Messages)
messageList.Add(
new
{
title = message.Title,
color = int.Parse(message.Color, NumberStyles.HexNumber),
description = message.Description
});
return messageList;
}
}
-7
View File
@@ -1,7 +0,0 @@
global using NUnit.Framework;
global using OpenQA.Selenium;
global using OpenQA.Selenium.Firefox;
global using OpenQA.Selenium.Chrome;
global using TestAutomation.Pages;
global using OpenQA.Selenium.Support.UI;
global using OpenQA.Selenium.Support;
-216
View File
@@ -1,216 +0,0 @@
using System.Collections.ObjectModel;
using OpenQA.Selenium.Interactions;
using TestAutomation.Enums;
using TestAutomation.Shared;
namespace TestAutomation.Utils;
public class Website
{
public static readonly DeploymentType DeploymentType =
Environment.GetEnvironmentVariable("TEST_HOOK")!.Contains("localhost")
? DeploymentType.Local
: DeploymentType.Dev;
public static readonly string Url =
DeploymentType.Equals(DeploymentType.Dev)
? "https://calm-mud-04916b210.1.azurestaticapps.net"
: "https://localhost:7234";
public readonly ScreenType ScreenType = ScreenType.Desktop;
public Website(IWebDriver webDriver, TestReport testReport)
{
WebDriver = webDriver;
TestReport = testReport;
// Pages
HarassCalculatorPage = new HarassCalculatorPage(this);
DatabasePage = new DatabasePage(this);
DatabaseSinglePage = new DatabaseSinglePage(this);
// Navigation
NavigationBar = new NavigationBar(this);
// Dialogs
WebsiteSearchDialog = new WebsiteSearchDialog(this);
}
public TestReport TestReport { get; set; }
public IWebDriver WebDriver { get; }
public HarassCalculatorPage HarassCalculatorPage { get; }
public DatabaseSinglePage DatabaseSinglePage { get; }
public DatabasePage DatabasePage { get; }
public NavigationBar NavigationBar { get; }
public WebsiteSearchDialog WebsiteSearchDialog { get; }
public IWebElement FindScreenSpecific(string byId)
{
var screenSpecificId = $"{ScreenType.ToString().ToLower()}-{byId}";
try
{
return WebDriver.FindElement(By.Id(screenSpecificId));
}
catch (Exception e)
{
throw new Exception($"Couldn't find {screenSpecificId}. Element does not exist on current page. " +
"\n\nPerhaps an Id is missing.");
}
}
public IWebElement Find(string byId, string withParentId)
{
IWebElement parent;
try
{
parent = WebDriver.FindElement(By.Id(withParentId));
}
catch (Exception e)
{
throw new Exception($"Couldn't find parent {withParentId}. Element does not exist on current page. " +
"\n\nPerhaps an Id is missing.");
}
try
{
return parent.FindElement(By.Id(byId));
}
catch (Exception e)
{
throw new Exception($"Couldn't find {byId}. Element does not exist on current page. " +
"\n\nPerhaps an Id is missing.");
}
}
public IWebElement Find(string byId)
{
try
{
return WebDriver.FindElement(By.Id(byId));
}
catch (Exception e)
{
throw new Exception($"Couldn't find {byId}. Element does not exist on current page. " +
"\n\nPerhaps an Id is missing.");
}
}
public ReadOnlyCollection<IWebElement> FindAll(string byId)
{
try
{
return WebDriver.FindElements(By.Id(byId));
}
catch (Exception e)
{
throw new Exception($"Couldn't find {byId}. Element does not exist on current page. " +
"\n\nPerhaps an Id is missing.");
}
}
public ReadOnlyCollection<IWebElement> FindAllWithTag(string tag)
{
return WebDriver.FindElements(By.TagName(tag));
}
public ReadOnlyCollection<IWebElement> FindAllWithTag(IWebElement parent, string tag)
{
return parent.FindElements(By.TagName(tag));
}
public IWebElement FindButtonWithLabel(string label)
{
try
{
return WebDriver.FindElement(By.XPath($"//button[@label='{label}']"));
}
catch (Exception e)
{
throw new Exception($"Couldn't find with label: {label}. Element does not exist on current page. ");
}
}
//@FindBy(xpath = "//div[@label='First Name']")
public IList<IWebElement> FindChildren(string ofId, string tagname)
{
return WebDriver.FindElements(By.CssSelector($"#{ofId} {tagname}"));
}
public string FindText(string byId)
{
return WebDriver.FindElement(By.Id(byId)).Text;
}
public int FindInt(string byId)
{
return int.Parse(WebDriver.FindElement(By.Id(byId)).Text);
}
public void ClickTopLeft()
{
new Actions(WebDriver)
.MoveByOffset(32, 32)
.Click()
.Perform();
}
public IWebElement Click(IWebElement element)
{
try
{
element.Click();
}
catch
{
throw new Exception($"Couldn't click on {element.GetDomProperty("id")}. ");
}
return element;
}
public IWebElement EnterInput<T>(IWebElement element, T input)
{
element.Clear();
element.SendKeys(input!.ToString());
element.SendKeys(Keys.Enter);
return element;
}
public IWebElement EnterInput<T>(string byId, T input)
{
var element = Find(byId);
element.Clear();
element.SendKeys(input!.ToString());
element.SendKeys(Keys.Enter);
return element;
}
public string GetLabel(string byId)
{
return Find(byId).Text;
}
public Website Goto()
{
WebDriver.Navigate().GoToUrl($"{Url}");
return this;
}
public void Goto(string path)
{
var url = $"{Url}/{path}";
WebDriver.Navigate().GoToUrl($"{url}");
}
}
-33
View File
@@ -1,33 +0,0 @@
using System.Text;
using Newtonsoft.Json;
namespace TestAutomation;
[SetUpFixture]
public class Tests : BaseTest
{
[OneTimeSetUp]
public void Setup()
{
Website.Goto();
Website.WebDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(15);
}
[OneTimeTearDown]
public void TearDown()
{
Website.WebDriver.Quit();
var message = new
{
content = "Test Report " + DateTime.Now.ToString("dd/MM/yyyy"),
embeds = TestReport.GetMessages()
};
var content = new StringContent(JsonConvert.SerializeObject(message), Encoding.UTF8, "application/json");
if (Environment.GetEnvironmentVariable("TEST_HOOK") == null) return;
HttpClient.PostAsync(Environment.GetEnvironmentVariable("TEST_HOOK"), content).Wait();
}
}
+23
View File
@@ -0,0 +1,23 @@
using NUnit.Framework;
using Tests.Helpers;
namespace Tests;
[SetUpFixture]
public class GlobalSetup
{
[OneTimeSetUp]
public async Task GlobalStart()
{
if (Environment.GetEnvironmentVariable("RUN_AGAINST_PRODUCTION") != "true")
{
await LocalServer.StartAsync();
}
}
[OneTimeTearDown]
public void GlobalStop()
{
LocalServer.Stop();
}
}
+106
View File
@@ -0,0 +1,106 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Tests.Helpers;
public static class LocalServer
{
private static Process? _process;
public static string? BaseUrl { get; private set; }
public static async Task StartAsync()
{
if (_process != null) return;
var root = FindProjectRoot();
var webProject = Path.Combine(root, "Web");
Console.WriteLine($"[DEBUG_LOG] Starting local server for project: {webProject}");
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"run --project \"{webProject}\" --urls http://127.0.0.1:0",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = root
}
};
var tcs = new TaskCompletionSource<string>();
_process.OutputDataReceived += (s, e) =>
{
if (string.IsNullOrEmpty(e.Data)) return;
Console.WriteLine($"[SERVER] {e.Data}");
var match = Regex.Match(e.Data, @"Now listening on:\s+(http://127.0.0.1:\d+)");
if (match.Success)
{
tcs.TrySetResult(match.Groups[1].Value);
}
};
_process.ErrorDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
Console.Error.WriteLine($"[SERVER ERROR] {e.Data}");
};
_process.Start();
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
// Wait for the URL to be parsed from output
BaseUrl = await Task.WhenAny(tcs.Task, Task.Delay(30000)) == tcs.Task
? tcs.Task.Result
: null;
if (BaseUrl == null)
{
Stop();
throw new Exception("Timeout waiting for local server to start and provide a URL.");
}
Console.WriteLine($"[DEBUG_LOG] Local server started at: {BaseUrl}");
}
public static void Stop()
{
if (_process != null && !_process.HasExited)
{
Console.WriteLine("[DEBUG_LOG] Stopping local server...");
_process.Kill(true);
_process.Dispose();
_process = null;
}
}
private static string FindProjectRoot()
{
var current = AppDomain.CurrentDomain.BaseDirectory;
while (!string.IsNullOrEmpty(current) && !File.Exists(Path.Combine(current, "IGP.sln")))
{
var parent = Path.GetDirectoryName(current);
if (parent == current) break;
current = parent;
}
if (string.IsNullOrEmpty(current) || !File.Exists(Path.Combine(current, "IGP.sln")))
{
// Fallback to searching up from current directory if BaseDirectory fails
current = Directory.GetCurrentDirectory();
while (!string.IsNullOrEmpty(current) && !File.Exists(Path.Combine(current, "IGP.sln")))
{
var parent = Path.GetDirectoryName(current);
if (parent == current) break;
current = parent;
}
}
return current ?? throw new Exception("Could not find project root containing IGP.sln");
}
}
+50
View File
@@ -0,0 +1,50 @@
using Microsoft.Playwright;
using Tests.Pages;
using Tests.Shared;
namespace Tests.Helpers;
public class Website
{
public IPage Page { get; }
public bool RunAgainstProduction { get; }
public string BaseUrl { get; }
public Website(IPage page)
{
Page = page;
RunAgainstProduction = Environment.GetEnvironmentVariable("RUN_AGAINST_PRODUCTION") == "true";
BaseUrl = RunAgainstProduction ? "https://igpfanreference.ca" : (LocalServer.BaseUrl ?? "http://localhost:5234");
NavigationBar = new NavigationBar(this);
SearchDialog = new SearchDialog(this);
BuildCalculatorPage = new BuildCalculatorPage(this);
HarassCalculatorPage = new HarassCalculatorPage(this);
DatabasePage = new DatabasePage(this);
DatabaseSinglePage = new DatabaseSinglePage(this);
}
public ILocator Locator(string selector) => Page.Locator(selector);
public ILocator FindById(string id) => Page.Locator($"#{id}");
public NavigationBar NavigationBar { get; }
public SearchDialog SearchDialog { get; }
public BuildCalculatorPage BuildCalculatorPage { get; }
public HarassCalculatorPage HarassCalculatorPage { get; }
public DatabasePage DatabasePage { get; }
public DatabaseSinglePage DatabaseSinglePage { get; }
public async Task GotoAsync(string? path = null)
{
var url = path is null ? BaseUrl : $"{BaseUrl}/{path}";
await Page.GotoAsync(url);
}
public async Task ClickElementAsync(ILocator locator) => await locator.ClickAsync();
public async Task EnterInputAsync(ILocator locator, string value)
{
await locator.FillAsync(value);
await locator.PressAsync("Enter");
}
}
+28
View File
@@ -0,0 +1,28 @@
using Tests.Helpers;
namespace Tests.Pages;
public abstract class BasePage
{
protected Website Website { get; }
protected BasePage(Website website)
{
Website = website;
}
public abstract string Url { get; }
public virtual async Task GotoAsync()
{
await Website.GotoAsync(Url);
}
public async Task<List<string>> GetLinksAsync()
{
var content = Website.FindById("content");
var links = content.Locator("a");
var hrefs = await links.EvaluateAllAsync<string[]>("els => els.map(el => el.getAttribute('href')).filter(Boolean)");
return hrefs.ToList();
}
}
@@ -0,0 +1,48 @@
namespace Tests.Pages.BuildCalculator;
public class ArmyComponent
{
private readonly Website _website;
public ArmyComponent(Website website) => _website = website;
public ILocator ArmyView => _website.Locator(".armyView");
public ILocator DisplayValue(string label) =>
_website.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent");
public ILocator ArmyCards => ArmyView.Locator(".armyCard");
public async Task<string> GetArmyCompletedAtAsync() =>
(await DisplayValue("Army Completed At").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetArmyAttackingAtAsync() =>
(await DisplayValue("Army Attacking At").TextContentAsync())?.Trim() ?? "";
public async Task<IReadOnlyList<string>> GetArmyUnitNamesAsync()
{
var cards = await ArmyCards.AllAsync();
var names = new List<string>();
foreach (var card in cards)
{
var text = (await card.InnerTextAsync()).Trim();
var match = System.Text.RegularExpressions.Regex.Match(text, @"\d+x\s*(.+)");
names.Add(match.Success ? match.Groups[1].Value.Trim() : text);
}
return names;
}
public async Task<IReadOnlyList<(string Name, int Count)>> GetArmyUnitCountsAsync()
{
var cards = await ArmyCards.AllAsync();
var counts = new List<(string, int)>();
foreach (var card in cards)
{
var countEl = card.Locator(".armyCount");
var nameEl = card.Locator("div").Last;
var count = (await countEl.TextContentAsync())?.Replace("x", "").Trim() ?? "0";
var name = (await nameEl.TextContentAsync())?.Trim() ?? "";
counts.Add((name, int.Parse(count)));
}
return counts;
}
}
@@ -0,0 +1,27 @@
namespace Tests.Pages.BuildCalculator;
public class BankComponent
{
private readonly Website _website;
public BankComponent(Website website) => _website = website;
public ILocator BankContainer => _website.Locator(".bankContainer");
public ILocator DisplayValue(string label) =>
BankContainer.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent");
public async Task<string> GetTimeAsync() => (await DisplayValue("Time").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetAlloyAsync() => (await DisplayValue("Alloy").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetEtherAsync() => (await DisplayValue("Ether").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetPyreAsync() => (await DisplayValue("Pyre").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetSupplyAsync() => (await DisplayValue("Supply").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetWorkerCountAsync() =>
(await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(0).TextContentAsync())?.Trim() ?? "";
public async Task<string> GetBusyWorkerCountAsync() =>
(await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(1).TextContentAsync())?.Trim() ?? "";
public async Task<string> GetCreatingWorkerCountAsync() =>
(await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(2).TextContentAsync())?.Trim() ?? "";
}
@@ -0,0 +1,20 @@
namespace Tests.Pages.BuildCalculator;
public class BuildChartComponent
{
private readonly Website _website;
public BuildChartComponent(Website website) => _website = website;
public ILocator ChartsContainer => _website.Locator(".chartsContainer");
public ILocator DisplayValue(string label) =>
_website.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent");
public async Task<string> GetHighestAlloyAsync() => (await DisplayValue("Highest Alloy").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetHighestEtherAsync() => (await DisplayValue("Highest Ether").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetHighestPyreAsync() => (await DisplayValue("Highest Pyre").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetHighestArmyAsync() => (await DisplayValue("Highest Army").TextContentAsync())?.Trim() ?? "";
public async Task<int> GetChartCountAsync() =>
await ChartsContainer.Locator("> div").CountAsync();
}
@@ -0,0 +1,12 @@
namespace Tests.Pages.BuildCalculator;
public class BuildOrderComponent
{
private readonly Website _website;
public BuildOrderComponent(Website website) => _website = website;
public ILocator JsonTextarea => _website.Locator("textarea");
public async Task<string> GetJsonDataAsync() =>
await JsonTextarea.InputValueAsync();
}
@@ -0,0 +1,31 @@
namespace Tests.Pages.BuildCalculator;
public class EntityClickViewComponent
{
private readonly Website _website;
public EntityClickViewComponent(Website website) => _website = website;
public ILocator EntityClickView => _website.Locator(".entityClickView");
public async Task<string?> GetEntityNameAsync()
{
var el = EntityClickView.Locator("#entityName");
if (await el.CountAsync() == 0) return null;
return (await el.TextContentAsync())?.Trim();
}
public async Task<string?> GetEntityHealthAsync()
{
var healthText = EntityClickView.Locator("div").Filter(new() { HasTextRegex = new System.Text.RegularExpressions.Regex("Health", System.Text.RegularExpressions.RegexOptions.IgnoreCase) }).First;
if (await healthText.CountAsync() == 0) return null;
var text = (await healthText.TextContentAsync()) ?? "";
var match = System.Text.RegularExpressions.Regex.Match(text, @"(\d+)");
return match.Success ? match.Groups[1].Value : null;
}
public async Task ClickDetailedViewAsync() =>
await EntityClickView.Locator("button").Filter(new() { HasText = "Detailed" }).ClickAsync();
public async Task ClickPlainViewAsync() =>
await EntityClickView.Locator("button").Filter(new() { HasText = "Plain" }).ClickAsync();
}
@@ -0,0 +1,28 @@
namespace Tests.Pages.BuildCalculator;
public class FilterComponent
{
private readonly Website _website;
public FilterComponent(Website website) => _website = website;
private ILocator FactionSelect =>
_website.Locator("select").Filter(new() { Has = _website.Locator("option:has-text('Aru'), option:has-text(\"Q'Rath\")") });
private ILocator ImmortalSelect =>
_website.Locator("select").Filter(new() { Has = _website.Locator("option:has-text('Orzum'), option:has-text('Ajari'), option:has-text('Atzlan'), option:has-text('Mala'), option:has-text('Xol')") });
public async Task SelectFactionAsync(string faction) =>
await FactionSelect.SelectOptionAsync(faction);
public async Task SelectImmortalAsync(string immortal) =>
await ImmortalSelect.SelectOptionAsync(immortal);
public async Task<string> GetSelectedFactionAsync() =>
await FactionSelect.InputValueAsync();
public async Task<string> GetSelectedImmortalAsync() =>
await ImmortalSelect.InputValueAsync();
public async Task<IReadOnlyList<string>> GetAvailableImmortalsAsync() =>
await ImmortalSelect.Locator("option").AllTextContentsAsync();
}
@@ -0,0 +1,29 @@
namespace Tests.Pages.BuildCalculator;
public class HighlightsComponent
{
private readonly Website _website;
public HighlightsComponent(Website website) => _website = website;
public ILocator HighlightsContainer => _website.Locator(".highlightsContainer");
public ILocator RequestedColumn => HighlightsContainer.Locator("div").Filter(new() { HasText = "Requested" }).Locator("+ div");
public ILocator FinishedColumn => HighlightsContainer.Locator("div").Filter(new() { HasText = "Finished" }).Locator("+ div");
public async Task<IReadOnlyList<string>> GetRequestedItemsAsync() =>
await GetHighlightItemsAsync();
public async Task<IReadOnlyList<string>> GetFinishedItemsAsync() =>
await GetHighlightItemsAsync();
private async Task<IReadOnlyList<string>> GetHighlightItemsAsync()
{
var items = await _website.Locator(".highlightsContainer").Locator("div").Filter(new() { HasTextRegex = new System.Text.RegularExpressions.Regex(@"^\d+\s*\|") }).AllAsync();
var result = new List<string>();
foreach (var item in items)
{
var text = (await item.TextContentAsync())?.Trim();
if (text is not null) result.Add(text);
}
return result;
}
}
@@ -0,0 +1,54 @@
namespace Tests.Pages.BuildCalculator;
public class HotkeyViewerComponent
{
private readonly Website _website;
public HotkeyViewerComponent(Website website) => _website = website;
public ILocator KeyContainer => _website.Locator(".keyContainer");
public async Task<ILocator?> FindKeyButtonAsync(string keyLabel)
{
var upper = keyLabel.ToUpperInvariant();
var buttons = KeyContainer.Locator("> div > div");
var count = await buttons.CountAsync();
for (int i = 0; i < count; i++)
{
var btn = buttons.Nth(i);
var text = (await btn.TextContentAsync())?.Trim().ToUpperInvariant() ?? "";
if (text.StartsWith(upper)) return btn;
}
return null;
}
public async Task ClickKeyAsync(string keyText)
{
var btn = await FindKeyButtonAsync(keyText);
if (btn is null) throw new InvalidOperationException($"Key \"{keyText}\" not found");
await btn.ClickAsync(new() { Force = true });
}
public async Task<string?> GetFirstEntityNameAsync(string keyText)
{
var btn = await FindKeyButtonAsync(keyText);
if (btn is null) return null;
var entities = btn.Locator("> div");
if (await entities.CountAsync() == 0) return null;
return (await entities.First.TextContentAsync())?.Trim();
}
public async Task<IReadOnlyList<string>> GetEntityNamesOnKeyAsync(string keyText)
{
var btn = await FindKeyButtonAsync(keyText);
if (btn is null) return Array.Empty<string>();
var entities = btn.Locator("> div");
var count = await entities.CountAsync();
var names = new List<string>();
for (int i = 0; i < count; i++)
{
var text = (await entities.Nth(i).TextContentAsync())?.Trim();
if (!string.IsNullOrEmpty(text)) names.Add(text);
}
return names;
}
}
@@ -0,0 +1,38 @@
namespace Tests.Pages.BuildCalculator;
public class OptionsComponent
{
private readonly Website _website;
public OptionsComponent(Website website) => _website = website;
private ILocator FormNumberInput(string label) =>
_website.Locator(".formNumberContainer").Filter(new() { HasText = label }).Locator("input[type='number']");
private ILocator ButtonWithLabel(string label) =>
_website.Locator("button").Filter(new() { HasText = label });
public ILocator BuildingInputDelayInput => FormNumberInput("Building Input Delay");
public ILocator WaitTimeInput => FormNumberInput("Wait Time");
public ILocator WaitToInput => FormNumberInput("Wait To");
public ILocator AddWaitButton => ButtonWithLabel("Add Wait").First;
public ILocator AddWaitToButton => ButtonWithLabel("Add Wait").Last;
public async Task SetBuildingInputDelayAsync(int value)
{
await BuildingInputDelayInput.FillAsync(value.ToString());
await BuildingInputDelayInput.PressAsync("Enter");
}
public async Task SetWaitTimeAsync(int value) =>
await WaitTimeInput.FillAsync(value.ToString());
public async Task SetWaitToAsync(int value) =>
await WaitToInput.FillAsync(value.ToString());
public async Task ClickAddWaitAsync() => await AddWaitButton.ClickAsync();
public async Task ClickAddWaitToAsync() => await AddWaitToButton.ClickAsync();
public async Task<string> GetBuildingInputDelayAsync() => await BuildingInputDelayInput.InputValueAsync();
public async Task<string> GetWaitTimeAsync() => await WaitTimeInput.InputValueAsync();
public async Task<string> GetWaitToAsync() => await WaitToInput.InputValueAsync();
}
@@ -0,0 +1,16 @@
namespace Tests.Pages.BuildCalculator;
public class TimelineComponent
{
private readonly Website _website;
public TimelineComponent(Website website) => _website = website;
public ILocator Container =>
_website.Locator(".calculatorGrid > div").Filter(new() { HasText = "Timeline highlights" });
public async Task<bool> ContainsEntityAsync(string name)
{
var text = (await Container.TextContentAsync()) ?? "";
return text.Contains(name);
}
}
@@ -0,0 +1,28 @@
namespace Tests.Pages.BuildCalculator;
public class TimingComponent
{
private readonly Website _website;
public TimingComponent(Website website) => _website = website;
private ILocator FormNumberInput(string label) =>
_website.Locator(".formNumberContainer").Filter(new() { HasText = label }).Locator("input[type='number']");
public ILocator AttackTimeInput => FormNumberInput("Attack Time");
public ILocator TravelTimeInput => FormNumberInput("Travel Time");
public async Task SetAttackTimeAsync(int value)
{
await AttackTimeInput.FillAsync(value.ToString());
await AttackTimeInput.PressAsync("Enter");
}
public async Task SetTravelTimeAsync(int value)
{
await TravelTimeInput.FillAsync(value.ToString());
await TravelTimeInput.PressAsync("Enter");
}
public async Task<string> GetAttackTimeAsync() => await AttackTimeInput.InputValueAsync();
public async Task<string> GetTravelTimeAsync() => await TravelTimeInput.InputValueAsync();
}
+43
View File
@@ -0,0 +1,43 @@
using Tests.Pages.BuildCalculator;
using Tests.Shared;
namespace Tests.Pages;
public class BuildCalculatorPage : BasePage
{
public BuildCalculatorPage(Website website) : base(website)
{
Timing = new TimingComponent(website);
Filter = new FilterComponent(website);
Options = new OptionsComponent(website);
Bank = new BankComponent(website);
Army = new ArmyComponent(website);
Highlights = new HighlightsComponent(website);
BuildOrder = new BuildOrderComponent(website);
Timeline = new TimelineComponent(website);
Hotkeys = new HotkeyViewerComponent(website);
EntityView = new EntityClickViewComponent(website);
Chart = new BuildChartComponent(website);
Toast = new ToastComponent(website);
}
public override string Url => "build-calculator";
public TimingComponent Timing { get; }
public FilterComponent Filter { get; }
public OptionsComponent Options { get; }
public BankComponent Bank { get; }
public ArmyComponent Army { get; }
public HighlightsComponent Highlights { get; }
public BuildOrderComponent BuildOrder { get; }
public TimelineComponent Timeline { get; }
public HotkeyViewerComponent Hotkeys { get; }
public EntityClickViewComponent EntityView { get; }
public BuildChartComponent Chart { get; }
public ToastComponent Toast { get; }
public ILocator CalculatorGrid => Website.Locator(".calculatorGrid");
public ILocator ClearBuildOrderButton => Website.Locator("button").Filter(new() { HasText = "Clear Build Order" });
public async Task ClickClearBuildOrderAsync() => await ClearBuildOrderButton.ClickAsync();
}
+26
View File
@@ -0,0 +1,26 @@
namespace Tests.Pages;
public class DatabasePage : BasePage
{
public DatabasePage(Website website) : base(website) { }
public override string Url => "database";
public async Task FilterNameAsync(string name)
{
var input = Website.FindById("filterName").First;
await Website.EnterInputAsync(input, name);
}
public async Task<string> GetEntityNameAsync(string entityType, string entityName)
{
var el = Website.Locator($"#{entityType.ToLower()}-{entityName.ToLower()}").Locator("#entityName");
return (await el.InnerTextAsync()).Trim();
}
public async Task<string> GetEntityNameByIndexAsync(int index)
{
var el = Website.FindById("entityName").Nth(index);
return (await el.InnerTextAsync()).Trim();
}
}
+25
View File
@@ -0,0 +1,25 @@
namespace Tests.Pages;
public class DatabaseSinglePage : BasePage
{
public DatabaseSinglePage(Website website) : base(website) { }
public override string Url => "database";
public async Task GotoWithSearchAsync(string searchText)
{
await Website.GotoAsync($"{Url}/{searchText}");
}
public async Task<string> GetEntityNameAsync() =>
(await Website.FindById("entityName").InnerTextAsync()).Trim();
public async Task<string> GetEntityHealthAsync() =>
(await Website.FindById("entityHealth").InnerTextAsync()).Trim();
public async Task<string> GetInvalidSearchAsync() =>
(await Website.FindById("invalidSearch").InnerTextAsync()).Trim();
public async Task<string> GetValidSearchAsync() =>
(await Website.FindById("validSearch").InnerTextAsync()).Trim();
}
+48
View File
@@ -0,0 +1,48 @@
namespace Tests.Pages;
public class HarassCalculatorPage : BasePage
{
public HarassCalculatorPage(Website website) : base(website) { }
public override string Url => "harass-calculator";
public async Task SetWorkersLostToHarassAsync(int number) =>
await EnterAndPressAsync("numberOfWorkersLostToHarass", number);
public async Task SetNumberOfTownHallsExistingAsync(int number) =>
await EnterAndPressAsync("numberOfTownHallsExisting", number);
public async Task SetTownHallTravelTimeAsync(int index, int seconds) =>
await EnterInputAtIndexAsync("numberOfTownHallTravelTimes", index, seconds);
public async Task<int> GetTotalAlloyHarassmentAsync() => await ReadIntAsync("totalAlloyHarassment");
public async Task<int> GetWorkerReplacementCostAsync() => await ReadIntAsync("workerReplacementCost");
public async Task<int> GetDelayedMiningCostAsync() => await ReadIntAsync("delayedMiningCost");
public async Task<int> GetAverageTravelTimeAsync() => await ReadIntAsync("getAverageTravelTime");
public async Task<int> GetExampleTotalAlloyLossAsync() => await ReadIntAsync("exampleTotalAlloyLoss");
public async Task<int> GetExampleWorkerCostAsync() => await ReadIntAsync("exampleWorkerCost");
public async Task<int> GetExampleMiningTimeCostAsync() => await ReadIntAsync("exampleMiningTimeCost");
public async Task<int> GetExampleTotalAlloyLossAccurateAsync() => await ReadIntAsync("exampleTotalAlloyLossAccurate");
public async Task<int> GetExampleTotalAlloyLossDifferenceAsync() => await ReadIntAsync("exampleTotalAlloyLossDifference");
public async Task<int> GetExampleTotalAlloyLossAccurateDifferenceAsync() => await ReadIntAsync("exampleTotalAlloyLossAccurateDifference");
private async Task EnterAndPressAsync(string id, int value)
{
var locator = Website.FindById(id);
await Website.EnterInputAsync(locator, value.ToString());
}
private async Task EnterInputAtIndexAsync(string parentId, int index, int value)
{
var inputs = Website.FindById(parentId).Locator("input");
var input = inputs.Nth(index);
await input.FillAsync(value.ToString());
await input.PressAsync("Enter");
}
private async Task<int> ReadIntAsync(string id)
{
var text = await Website.FindById(id).TextContentAsync() ?? "";
return int.Parse(text.Trim());
}
}
+20
View File
@@ -0,0 +1,20 @@
namespace Tests.Shared;
public class NavigationBar
{
private readonly Website _website;
public NavigationBar(Website website) => _website = website;
public ILocator SearchButton => _website.Locator("#desktop-searchButton");
public async Task ClickHomeLinkAsync()
{
await _website.Locator("a:has-text(\"IGP Fan Reference\")").ClickAsync();
}
public async Task<SearchDialog> ClickSearchButtonAsync()
{
await SearchButton.ClickAsync();
return _website.SearchDialog;
}
}
+26
View File
@@ -0,0 +1,26 @@
namespace Tests.Shared;
public class SearchDialog
{
private readonly Website _website;
public SearchDialog(Website website) => _website = website;
public ILocator SearchBackground => _website.FindById("searchBackground");
public ILocator SearchInput => _website.FindById("searchInput");
public async Task CloseDialogAsync()
{
await _website.ClickElementAsync(SearchBackground);
}
public async Task<SearchDialog> SearchAsync(string text)
{
await _website.EnterInputAsync(SearchInput, text);
return this;
}
public async Task SelectSearchEntityAsync(string label)
{
await _website.ClickElementAsync(_website.Locator($"button[label=\"{label}\"]"));
}
}
+36
View File
@@ -0,0 +1,36 @@
namespace Tests.Shared;
public class ToastComponent
{
private readonly Website _website;
public ToastComponent(Website website) => _website = website;
public ILocator Container => _website.Locator(".toastsContainer");
public ILocator Toasts => _website.Locator(".toastsContainer .toastContainer");
public async Task<IReadOnlyList<string>> GetToastTitlesAsync()
{
var titles = await _website.Locator(".toastsContainer .toastTitle").AllTextContentsAsync();
return titles.Select(t => t.Trim()).Where(t => !string.IsNullOrEmpty(t)).ToList();
}
public async Task<bool> HasToastContainingAsync(string text)
{
try
{
await _website.Page.WaitForFunctionAsync(
@"(expected) => {
const titles = document.querySelectorAll('.toastsContainer .toastTitle');
return Array.from(titles).some(t => t.textContent.trim().includes(expected));
}",
text,
new() { Timeout = 3000 }
);
return true;
}
catch
{
return false;
}
}
}
+115
View File
@@ -0,0 +1,115 @@
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace Tests.Specs;
[Parallelizable(ParallelScope.Self)]
[FixtureLifeCycle(LifeCycle.SingleInstance)]
public class BuildCalculatorTests : PageTest
{
private Helpers.Website _website = null!;
[SetUp]
public void CreateWebsite() => _website = new Helpers.Website(Page);
[Test]
public async Task AddEntitiesViaKeyboardQWE()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
await calc.Hotkeys.ClickKeyAsync("TAB");
var keyMap = new Dictionary<string, string> { ["Q"] = "q", ["W"] = "w", ["E"] = "e", ["TAB"] = "Tab" };
foreach (var key in new[] { "Q", "W", "E", "TAB" })
{
var entityNames = await calc.Hotkeys.GetEntityNamesOnKeyAsync(key);
if (entityNames.Count == 0) continue;
await Page.Keyboard.PressAsync(keyMap[key]);
var viewName = await calc.EntityView.GetEntityNameAsync();
Assert.That(viewName, Is.Not.Null.And.Not.Empty);
Assert.That(entityNames, Does.Contain(viewName));
}
}
[Test]
public async Task AddEntitiesViaHotkeysClickTABQWE()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
foreach (var key in new[] { "TAB", "Q", "W", "E" })
{
var entityNames = await calc.Hotkeys.GetEntityNamesOnKeyAsync(key);
if (entityNames.Count == 0) continue;
await calc.Hotkeys.ClickKeyAsync(key);
var viewName = await calc.EntityView.GetEntityNameAsync();
Assert.That(viewName, Is.Not.Null.And.Not.Empty);
Assert.That(entityNames, Does.Contain(viewName));
}
}
[Test]
public async Task AddAcropolisViaQVerifyEntityViewAndTimelineThenClear()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
Assert.That(await calc.Timeline.ContainsEntityAsync("Acropolis"), Is.False);
await calc.Hotkeys.ClickKeyAsync("Q");
Assert.That(await calc.EntityView.GetEntityNameAsync(), Is.EqualTo("Acropolis"));
Assert.That(await calc.Timeline.ContainsEntityAsync("Acropolis"), Is.True);
await calc.ClickClearBuildOrderAsync();
await Task.Delay(1000);
Assert.That(await calc.Timeline.ContainsEntityAsync("Acropolis"), Is.False);
Assert.That(await calc.EntityView.GetEntityNameAsync(), Is.Null);
}
[Test]
public async Task MissingRequirementsToastWhenBuildingSoulFoundryWithoutLegionHall()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
await calc.Hotkeys.ClickKeyAsync("E");
var hasToast = await calc.Toast.HasToastContainingAsync("Missing Requirements");
Assert.That(hasToast, Is.True);
}
[Test]
public async Task NotEnoughEtherToastWhenBuildingSoulFoundryAfterLegionHall()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
await calc.Hotkeys.ClickKeyAsync("W");
await calc.Hotkeys.ClickKeyAsync("E");
var hasToast = await calc.Toast.HasToastContainingAsync("Not Enough Ether");
Assert.That(hasToast, Is.True);
}
}
+43
View File
@@ -0,0 +1,43 @@
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace Tests.Specs;
[Parallelizable(ParallelScope.Self)]
[FixtureLifeCycle(LifeCycle.SingleInstance)]
public class HarassCalculatorTests : PageTest
{
private Helpers.Website _website = null!;
[SetUp]
public void CreateWebsite() => _website = new Helpers.Website(Page);
[Test]
public async Task CalculatorInput()
{
var page = _website.HarassCalculatorPage;
await page.GotoAsync();
await page.SetWorkersLostToHarassAsync(3);
await page.SetNumberOfTownHallsExistingAsync(2);
await page.SetTownHallTravelTimeAsync(0, 30);
var result = await page.GetTotalAlloyHarassmentAsync();
Assert.That(result, Is.EqualTo(240));
}
[Test]
public async Task CalculatedExampleInformation()
{
var page = _website.HarassCalculatorPage;
await page.GotoAsync();
Assert.Multiple(async () =>
{
Assert.That(await page.GetExampleTotalAlloyLossAsync(), Is.EqualTo(720));
Assert.That(await page.GetExampleWorkerCostAsync(), Is.EqualTo(300));
Assert.That(await page.GetExampleMiningTimeCostAsync(), Is.EqualTo(420));
Assert.That(await page.GetExampleTotalAlloyLossAccurateAsync(), Is.EqualTo(450));
Assert.That(await page.GetExampleTotalAlloyLossDifferenceAsync(), Is.EqualTo(300));
Assert.That(await page.GetExampleTotalAlloyLossAccurateDifferenceAsync(), Is.EqualTo(270));
});
}
}
+41
View File
@@ -0,0 +1,41 @@
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace Tests.Specs;
[Parallelizable(ParallelScope.Self)]
[FixtureLifeCycle(LifeCycle.SingleInstance)]
public class LinksTests : PageTest
{
private Helpers.Website _website = null!;
[SetUp]
public void CreateWebsite() => _website = new Helpers.Website(Page);
[Test]
public async Task VerifyPageLinks()
{
_website = new Helpers.Website(Page);
await _website.HarassCalculatorPage.GotoAsync();
var harassLinks = await _website.HarassCalculatorPage.GetLinksAsync();
foreach (var link in harassLinks) await VerifyLinkAsync(link);
await _website.DatabasePage.GotoAsync();
var dbLinks = await _website.DatabasePage.GetLinksAsync();
foreach (var link in dbLinks) await VerifyLinkAsync(link);
await _website.DatabaseSinglePage.GotoWithSearchAsync("throne");
var singleLinks = await _website.DatabaseSinglePage.GetLinksAsync();
foreach (var link in singleLinks) await VerifyLinkAsync(link);
}
private static async Task VerifyLinkAsync(string link)
{
if (link.StartsWith("mailto")) return;
using var client = new System.Net.Http.HttpClient();
var response = await client.GetAsync(link);
Assert.That(response.IsSuccessStatusCode, Is.True, $"Link '{link}' returned {response.StatusCode}");
}
}
+68
View File
@@ -0,0 +1,68 @@
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace Tests.Specs;
[Parallelizable(ParallelScope.Self)]
[FixtureLifeCycle(LifeCycle.SingleInstance)]
public class SearchFeaturesTests : PageTest
{
private Helpers.Website _website = null!;
[SetUp]
public void CreateWebsite() => _website = new Helpers.Website(Page);
[Test]
public async Task DesktopOpenCloseSearchDialog()
{
await _website.GotoAsync();
await _website.NavigationBar.ClickSearchButtonAsync();
await _website.SearchDialog.CloseDialogAsync();
await _website.NavigationBar.ClickHomeLinkAsync();
}
[Test]
public async Task DesktopSearchForThrone()
{
await _website.GotoAsync();
await _website.NavigationBar.ClickSearchButtonAsync();
await _website.SearchDialog.SearchAsync("Throne");
await _website.SearchDialog.SelectSearchEntityAsync("Throne");
var name = await _website.DatabaseSinglePage.GetEntityNameAsync();
var health = await _website.DatabaseSinglePage.GetEntityHealthAsync();
Assert.That(name, Is.EqualTo("Throne"));
Assert.That(health.Trim(), Is.Not.Empty);
}
[Test]
public async Task DesktopFilterForThrone()
{
var page = _website.DatabasePage;
await page.GotoAsync();
await page.FilterNameAsync("Throne");
var name = await page.GetEntityNameByIndexAsync(0);
Assert.That(name, Is.EqualTo("Throne"));
}
[Test]
public async Task SeeThroneByDefault()
{
var page = _website.DatabasePage;
await page.GotoAsync();
var name = await page.GetEntityNameAsync("army", "throne");
Assert.That(name, Is.EqualTo("Throne"));
}
[Test]
public async Task DirectLinkNotThroneFailure()
{
var page = _website.DatabaseSinglePage;
await page.GotoWithSearchAsync("not throne");
var invalidSearch = await page.GetInvalidSearchAsync();
var validSearch = await page.GetValidSearchAsync();
Assert.That(invalidSearch, Is.EqualTo("not throne"));
Assert.That(validSearch, Is.EqualTo("Throne"));
}
}
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.52.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.Playwright" />
<Using Include="Tests.Helpers" />
</ItemGroup>
</Project>
+9
View File
@@ -44,6 +44,15 @@
:root { :root {
--faction-aru: #da4e4e;
--immortal-mala: #dc7a29;
--immortal-xol: #87aa87;
--immortal-atzlan: #8B7355;
--faction-qrath: #8EACCD;
--immortal-orzum: #4A6B8A;
--immortal-ajari: #b4e2e3;
--severity-warning-color: #2a2000; --severity-warning-color: #2a2000;
--severity-warning-border-color: #755c13; --severity-warning-border-color: #755c13;
--severity-error-color: #290102; --severity-error-color: #290102;
View File
View File
@@ -13,6 +13,7 @@
@inject IDataCollectionService DataCollectionService @inject IDataCollectionService DataCollectionService
@page "/build-calculator" @page "/build-calculator"
@using IGP.Pages.BuildCalculator.Parts.Cosmetic
@using Services.Website @using Services.Website
@implements IDisposable @implements IDisposable
@@ -23,6 +24,14 @@
<div class="calculatorGrid"> <div class="calculatorGrid">
<div class="gridItem" style="grid-area: timing;"> <div class="gridItem" style="grid-area: timing;">
<PanelComponent>
<InfoTooltipComponent InfoText="@Locale["Tooltip Filter Info"]">
<FactionBorderComponent>
<FilterComponent></FilterComponent>
</FactionBorderComponent>
</InfoTooltipComponent>
</PanelComponent>
<ButtonComponent MyButtonType="MyButtonType.Secondary" OnClick="OnResetClicked">Clear Build Order <ButtonComponent MyButtonType="MyButtonType.Secondary" OnClick="OnResetClicked">Clear Build Order
</ButtonComponent> </ButtonComponent>
<PanelComponent> <PanelComponent>
@@ -30,11 +39,6 @@
<TimingComponent></TimingComponent> <TimingComponent></TimingComponent>
</InfoTooltipComponent> </InfoTooltipComponent>
</PanelComponent> </PanelComponent>
<PanelComponent>
<InfoTooltipComponent InfoText="@Locale["Tooltip Filter Info"]">
<FilterComponent></FilterComponent>
</InfoTooltipComponent>
</PanelComponent>
<PanelComponent> <PanelComponent>
<InfoTooltipComponent InfoText="@Locale["Tooltip Options Info"]"> <InfoTooltipComponent InfoText="@Locale["Tooltip Options Info"]">
@@ -55,7 +59,9 @@
<div class="gridItem" style="grid-area: view;"> <div class="gridItem" style="grid-area: view;">
<PanelComponent> <PanelComponent>
<InfoTooltipComponent InfoText="@Locale["Tooltip Entity Info"]"> <InfoTooltipComponent InfoText="@Locale["Tooltip Entity Info"]">
<ImmortalBorderComponent>
<EntityClickViewComponent/> <EntityClickViewComponent/>
</ImmortalBorderComponent>
</InfoTooltipComponent> </InfoTooltipComponent>
</PanelComponent> </PanelComponent>
@@ -231,13 +231,13 @@ else
var alloyAutomaticHarvesters = from harvester in economyAtSecond.HarvestPoints var alloyAutomaticHarvesters = from harvester in economyAtSecond.HarvestPoints
where harvester.Harvest() != null where harvester.Harvest() != null
where harvester.Harvest().RequiresWorker == false where !harvester.Harvest().RequiresWorker
where harvester.Harvest().Resource == ResourceType.Alloy where harvester.Harvest().Resource == ResourceType.Alloy
select harvester; select harvester;
var etherAutomaticHarvesters = from harvester in economyAtSecond.HarvestPoints var etherAutomaticHarvesters = from harvester in economyAtSecond.HarvestPoints
where harvester.Harvest() != null where harvester.Harvest() != null
where harvester.Harvest().RequiresWorker == false where !harvester.Harvest().RequiresWorker
where harvester.Harvest().Resource == ResourceType.Ether where harvester.Harvest().Resource == ResourceType.Ether
select harvester; select harvester;
@@ -9,14 +9,14 @@
</FormLayoutComponent> </FormLayoutComponent>
@code { @code {
/**
// TODO: Make this more elegant, and useful. Also, it currently doesn't clear properly
<FormTextAreaComponent Label="JSON Data"
Rows="14"
Value="@buildOrderService.AsJson()">
</FormTextAreaComponent>
*/
/**
* // TODO: Make this more elegant, and useful. Also, it currently doesn't clear properly
* <FormTextAreaComponent Label="JSON Data"
* Rows="14"
* Value="@buildOrderService.AsJson()">
* </FormTextAreaComponent>
*/
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();
@@ -0,0 +1,32 @@
@inject IImmortalSelectionService FilterService
@implements IDisposable
<FormLayoutComponent>
<div style="@GetBorderStyle()">
@ChildContent
</div>
</FormLayoutComponent>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
FilterService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
FilterService.Unsubscribe(StateHasChanged);
}
string GetBorderStyle()
{
var faction = FilterService.GetFaction();
var color = faction == DataType.FACTION_Aru ? "var(--faction-aru)" : "var(--faction-qrath)";
return $"border-top: 4px solid {color}; padding-top: 14px; margin-top: -12px;";
}
}
@@ -0,0 +1,37 @@
@inject IImmortalSelectionService FilterService
@implements IDisposable
<FormLayoutComponent>
<div style="@GetBorderStyle()">
@ChildContent
</div>
</FormLayoutComponent>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
FilterService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
FilterService.Unsubscribe(StateHasChanged);
}
string GetBorderStyle()
{
var immortal = FilterService.GetImmortal();
var color = "#666666";
if (immortal == DataType.IMMORTAL_Orzum) color = "var(--immortal-orzum)";
else if (immortal == DataType.IMMORTAL_Ajari) color = "var(--immortal-ajari)";
else if (immortal == DataType.IMMORTAL_Atzlan) color = "var(--immortal-atzlan)";
else if (immortal == DataType.IMMORTAL_Mala) color = "var(--immortal-mala)";
else if (immortal == DataType.IMMORTAL_Xol) color = "var(--immortal-xol)";
return $"border-top: 4px solid {color}; padding-top: 14px; margin-top: -12px;";
}
}

Some files were not shown because too many files have changed in this diff Show More