Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85834466f1 | |||
| 7da6f554a8 | |||
| dc0395c7d3 | |||
| 026aebd5ad | |||
| 3974fcfb91 | |||
| 410e7e23b7 | |||
| 6655cdeee7 | |||
| 1f7a0819fc | |||
| 73f29cea08 |
@@ -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/
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<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>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet publish IGP/IGP.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
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using Model.Entity;
|
||||||
|
using Model.Entity.Data;
|
||||||
|
using Services;
|
||||||
|
using Services.Immortal;
|
||||||
|
using Services.Website;
|
||||||
|
using IGP.Calculator.Cli.Services;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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() => Task.CompletedTask;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
+8
-7
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Blazor-Analytics" Version="3.11.0"/>
|
<PackageReference Include="Blazor-Analytics" Version="4.0.0" />
|
||||||
<PackageReference Include="Markdig" Version="0.30.3"/>
|
<PackageReference Include="Markdig" Version="1.1.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.14"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.14"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.14"/>
|
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
|
||||||
<PackageReference Include="MudBlazor" Version="8.5.1"/>
|
<PackageReference Include="MudBlazor" Version="9.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Pages\DataTables\Parts\" />
|
||||||
<Folder Include="wwwroot\generated" />
|
<Folder Include="wwwroot\generated" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
+49
-5
@@ -11,34 +11,78 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components", "..\Components
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services", "..\Services\Services.csproj", "{621178C8-4E8B-478E-80E5-7478F0E7B67E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services", "..\Services\Services.csproj", "{621178C8-4E8B-478E-80E5-7478F0E7B67E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAutomation", "..\TestAutomation\TestAutomation.csproj", "{8B49D038-D013-460D-9C4F-817CAFFEB06F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IGP.Calculator.Cli", "..\IGP.Calculator.Cli\IGP.Calculator.Cli.csproj", "{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{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}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|x86.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.ActiveCfg = Release|Any CPU
|
||||||
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|Any CPU.Build.0 = Release|Any CPU
|
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|x86.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.ActiveCfg = Debug|Any CPU
|
||||||
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|x86.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.ActiveCfg = Release|Any CPU
|
||||||
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|Any CPU.Build.0 = Release|Any CPU
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|x86.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.ActiveCfg = Debug|Any CPU
|
||||||
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|x86.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.ActiveCfg = Release|Any CPU
|
||||||
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|x86.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.ActiveCfg = Debug|Any CPU
|
||||||
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|x86.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.ActiveCfg = Release|Any CPU
|
||||||
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|Any CPU.Build.0 = 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
|
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{8B49D038-D013-460D-9C4F-817CAFFEB06F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{8B49D038-D013-460D-9C4F-817CAFFEB06F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{8B49D038-D013-460D-9C4F-817CAFFEB06F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -13,31 +13,25 @@
|
|||||||
@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
|
||||||
|
|
||||||
<LayoutLargeContentComponent>
|
<LayoutLargeContentComponent>
|
||||||
<WebsiteTitleComponent>Build Calculator</WebsiteTitleComponent>
|
<WebsiteTitleComponent>Build Calculator</WebsiteTitleComponent>
|
||||||
|
|
||||||
<AlertComponent Type="@SeverityType.Warning">
|
|
||||||
<Title>Work In Progress and Not Fully Tested</Title>
|
|
||||||
<Message>
|
|
||||||
Build Calculator hasn't been thoroughly tested. Bugs and inaccurate results assumed.
|
|
||||||
<br/>
|
|
||||||
Currently not considering running out of alloy and ether to harvest.
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
Build Calculator was built based on a much older version of the game and was only quickly modified for the
|
|
||||||
June 2025 Playtest version, so the above disclaimer is only more true.
|
|
||||||
<br/>
|
|
||||||
Expect even more oddities and invalid data then the above warning implies.
|
|
||||||
</Message>
|
|
||||||
</AlertComponent>
|
|
||||||
|
|
||||||
<ContentDividerComponent></ContentDividerComponent>
|
<ContentDividerComponent></ContentDividerComponent>
|
||||||
|
|
||||||
<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>
|
||||||
@@ -45,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"]">
|
||||||
@@ -70,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>
|
||||||
|
|||||||
@@ -5,13 +5,17 @@
|
|||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<FormLayoutComponent>
|
<FormLayoutComponent>
|
||||||
|
|
||||||
|
</FormLayoutComponent>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/**
|
||||||
|
// TODO: Make this more elegant, and useful. Also, it currently doesn't clear properly
|
||||||
<FormTextAreaComponent Label="JSON Data"
|
<FormTextAreaComponent Label="JSON Data"
|
||||||
Rows="14"
|
Rows="14"
|
||||||
Value="@buildOrderService.AsJson()">
|
Value="@buildOrderService.AsJson()">
|
||||||
</FormTextAreaComponent>
|
</FormTextAreaComponent>
|
||||||
</FormLayoutComponent>
|
*/
|
||||||
|
|
||||||
@code {
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@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,36 @@
|
|||||||
|
@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;";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
@inject IJSRuntime JsRuntime
|
@inject IJSRuntime JsRuntime
|
||||||
@inject IKeyService KeyService
|
@inject IKeyService KeyService
|
||||||
@inject IImmortalSelectionService FilterService
|
@inject IImmortalSelectionService FilterService
|
||||||
@inject IBuildOrderService BuildOrderService
|
|
||||||
@inject IStorageService StorageService
|
@inject IStorageService StorageService
|
||||||
|
@inject IBuildOrderService BuildOrderService
|
||||||
@using Services.Website
|
@using Services.Website
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
base.OnInitialized();
|
base.OnInitialized();
|
||||||
KeyService.Subscribe(HandleClick);
|
KeyService.Subscribe(HandleClick);
|
||||||
StorageService.Subscribe(RefreshDefaults);
|
StorageService.Subscribe(RefreshDefaults);
|
||||||
|
BuildOrderService.Subscribe(OnBuildOrderServiceChanged);
|
||||||
|
|
||||||
RefreshDefaults();
|
RefreshDefaults();
|
||||||
}
|
}
|
||||||
@@ -41,31 +42,25 @@
|
|||||||
void IDisposable.Dispose()
|
void IDisposable.Dispose()
|
||||||
{
|
{
|
||||||
KeyService.Unsubscribe(HandleClick);
|
KeyService.Unsubscribe(HandleClick);
|
||||||
|
|
||||||
StorageService.Unsubscribe(RefreshDefaults);
|
StorageService.Unsubscribe(RefreshDefaults);
|
||||||
|
BuildOrderService.Unsubscribe(OnBuildOrderServiceChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void OnBuildOrderServiceChanged()
|
||||||
|
{
|
||||||
|
if (BuildOrderService.GetLastRequestInterval() == 0)
|
||||||
|
{
|
||||||
|
_entity = null;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RefreshDefaults()
|
void RefreshDefaults()
|
||||||
{
|
{
|
||||||
_viewType = StorageService.GetValue<bool>(StorageKeys.IsPlainView) ? EntityViewType.Plain : EntityViewType.Detailed;
|
_viewType = StorageService.GetValue<bool>(StorageKeys.IsPlainView) ? EntityViewType.Plain : EntityViewType.Detailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool ShouldRender()
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
JsRuntime.InvokeVoidAsync("console.time", "EntityClickViewComponent");
|
|
||||||
#endif
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnAfterRender(bool firstRender)
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
JsRuntime.InvokeVoidAsync("console.timeEnd", "EntityClickViewComponent");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleClick()
|
private void HandleClick()
|
||||||
{
|
{
|
||||||
var hotkey = KeyService.GetHotkey();
|
var hotkey = KeyService.GetHotkey();
|
||||||
@@ -82,5 +77,4 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
@inject IImmortalSelectionService FilterService
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
<FormLayoutComponent>
|
||||||
|
<InfoBodyComponent>
|
||||||
|
<InfoQuestionComponent>
|
||||||
|
What is this tool?
|
||||||
|
</InfoQuestionComponent>
|
||||||
|
<InfoAnswerComponent>
|
||||||
|
This is a calculator to determine build timings. Mostly so someone can quickly try out a few build
|
||||||
|
orders to see if they somewhat make sense.
|
||||||
|
</InfoAnswerComponent>
|
||||||
|
</InfoBodyComponent>
|
||||||
|
|
||||||
|
<InfoBodyComponent>
|
||||||
|
<InfoQuestionComponent>
|
||||||
|
How does it work?
|
||||||
|
</InfoQuestionComponent>
|
||||||
|
<InfoAnswerComponent>
|
||||||
|
The tool calculates every second of game time. So if you attempt to build a <b>Legion Hall</b> as
|
||||||
|
your first action, the tool will scan every second, until you get to one where the request can be
|
||||||
|
made. In this case, that is interval 58.
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
If you then build 2 <b>Apostle of Bindings</b> a <b>Soul Foundry</b> and a 3 <b>Absolvers</b> you
|
||||||
|
should see yourself roughly floating 500 alloy, with barely having any ether. Which means you could
|
||||||
|
of gotten an <b>Acropolis</b> and a <b>Zentari</b> without hurting your build.
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
Try building <b>Apostle of Bindings</b> before the <b>Legion Hall</b> and see how that changes the
|
||||||
|
timing of your 3 <b>Absolvers</b>. (Spoiler:
|
||||||
|
<SpoilerTextComponent> your <b>Absolvers</b> will be built much faster, and you won't be floating so
|
||||||
|
much alloy.
|
||||||
|
</SpoilerTextComponent>
|
||||||
|
)
|
||||||
|
</InfoAnswerComponent>
|
||||||
|
</InfoBodyComponent>
|
||||||
|
|
||||||
|
<InfoBodyComponent>
|
||||||
|
<InfoQuestionComponent>
|
||||||
|
What is CONTROL key for?
|
||||||
|
</InfoQuestionComponent>
|
||||||
|
<InfoAnswerComponent>
|
||||||
|
Economy and tech related upgrades for townhalls.
|
||||||
|
</InfoAnswerComponent>
|
||||||
|
</InfoBodyComponent>
|
||||||
|
|
||||||
|
<InfoBodyComponent>
|
||||||
|
<InfoQuestionComponent>
|
||||||
|
What is SHIFT key for?
|
||||||
|
</InfoQuestionComponent>
|
||||||
|
<InfoAnswerComponent>
|
||||||
|
Misc building related upgrades. (Omnivores)
|
||||||
|
</InfoAnswerComponent>
|
||||||
|
</InfoBodyComponent>
|
||||||
|
|
||||||
|
<InfoBodyComponent>
|
||||||
|
<InfoQuestionComponent>
|
||||||
|
What is 2 key for?
|
||||||
|
</InfoQuestionComponent>
|
||||||
|
<InfoAnswerComponent>
|
||||||
|
It will be for Pyre camps. Currently not implemented.
|
||||||
|
</InfoAnswerComponent>
|
||||||
|
</InfoBodyComponent>
|
||||||
|
</FormLayoutComponent>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
FilterService.Subscribe(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDisposable.Dispose()
|
||||||
|
{
|
||||||
|
FilterService.Unsubscribe(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,14 +6,6 @@
|
|||||||
|
|
||||||
<LayoutLargeContentComponent>
|
<LayoutLargeContentComponent>
|
||||||
<WebsiteTitleComponent>Data Tables</WebsiteTitleComponent>
|
<WebsiteTitleComponent>Data Tables</WebsiteTitleComponent>
|
||||||
|
|
||||||
<AlertComponent Type="@SeverityType.Warning">
|
|
||||||
<Title>Errors Present</Title>
|
|
||||||
<Message>
|
|
||||||
Incomplete feature for easily comparing unit stats.
|
|
||||||
</Message>
|
|
||||||
</AlertComponent>
|
|
||||||
|
|
||||||
<MudTabs Elevation="2">
|
<MudTabs Elevation="2">
|
||||||
<MudTabPanel Text="Attacks">
|
<MudTabPanel Text="Attacks">
|
||||||
<WeaponTable/>
|
<WeaponTable/>
|
||||||
@@ -29,7 +21,6 @@
|
|||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
</MudTabs>
|
</MudTabs>
|
||||||
|
|
||||||
|
|
||||||
<ContentDividerComponent></ContentDividerComponent>
|
<ContentDividerComponent></ContentDividerComponent>
|
||||||
|
|
||||||
<PaperComponent>
|
<PaperComponent>
|
||||||
@@ -43,10 +34,7 @@
|
|||||||
attack belongs to.
|
attack belongs to.
|
||||||
</InfoAnswerComponent>
|
</InfoAnswerComponent>
|
||||||
</InfoBodyComponent>
|
</InfoBodyComponent>
|
||||||
|
|
||||||
</PaperComponent>
|
</PaperComponent>
|
||||||
|
|
||||||
|
|
||||||
</LayoutLargeContentComponent>
|
</LayoutLargeContentComponent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -9,13 +9,6 @@
|
|||||||
<LayoutMediumContentComponent>
|
<LayoutMediumContentComponent>
|
||||||
<WebsiteTitleComponent>Harass Calculator</WebsiteTitleComponent>
|
<WebsiteTitleComponent>Harass Calculator</WebsiteTitleComponent>
|
||||||
|
|
||||||
<AlertComponent Type="@SeverityType.Warning">
|
|
||||||
<Title>Might be out of date</Title>
|
|
||||||
<Message>
|
|
||||||
This calculation is from several years ago and might not reflect the current state of the game.
|
|
||||||
</Message>
|
|
||||||
</AlertComponent>
|
|
||||||
|
|
||||||
<PaperComponent>
|
<PaperComponent>
|
||||||
Credit to Zard for deriving the formula.
|
Credit to Zard for deriving the formula.
|
||||||
</PaperComponent>
|
</PaperComponent>
|
||||||
|
|||||||
@@ -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,6 +1,9 @@
|
|||||||
namespace Model.Entity.Parts;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Model.Entity.Parts;
|
||||||
|
|
||||||
public class IEntityPartInterface
|
public class IEntityPartInterface
|
||||||
{
|
{
|
||||||
|
[JsonIgnore]
|
||||||
public EntityModel Parent { get; set; }
|
public EntityModel Parent { get; set; }
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1"/>
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1"/>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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); });
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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); });
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
const ScreenType = Object.freeze({ Desktop: 'desktop', Tablet: 'tablet', Mobile: 'mobile' });
|
||||||
|
|
||||||
|
class Website {
|
||||||
|
constructor(page, options = {}) {
|
||||||
|
this.page = page;
|
||||||
|
this.screenType = ScreenType.Desktop;
|
||||||
|
this.runAgainstProduction = options.production || process.env.RUN_AGAINST_PRODUCTION === 'true';
|
||||||
|
|
||||||
|
if (this.runAgainstProduction) {
|
||||||
|
this.baseUrl = 'https://igpfanreference.ca';
|
||||||
|
} else {
|
||||||
|
const hook = process.env.TEST_HOOK || '';
|
||||||
|
this.deploymentType = hook.includes('localhost') ? 'Local' : 'Dev';
|
||||||
|
this.baseUrl = 'https://localhost:7234';
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuildCalculatorPage = require('../pages/buildCalculatorPage');
|
||||||
|
const HarassCalculatorPage = require('../pages/harassCalculator.page');
|
||||||
|
const DatabasePage = require('../pages/database.page');
|
||||||
|
const DatabaseSinglePage = require('../pages/databaseSingle.page');
|
||||||
|
const NavigationBar = require('../shared/navigationBar');
|
||||||
|
const WebsiteSearchDialog = require('../shared/websiteSearchDialog');
|
||||||
|
|
||||||
|
this.buildCalculatorPage = new BuildCalculatorPage(this);
|
||||||
|
this.harassCalculatorPage = new HarassCalculatorPage(this);
|
||||||
|
this.databasePage = new DatabasePage(this);
|
||||||
|
this.databaseSinglePage = new DatabaseSinglePage(this);
|
||||||
|
this.navigationBar = new NavigationBar(this);
|
||||||
|
this.websiteSearchDialog = new WebsiteSearchDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
locator(selector) {
|
||||||
|
return this.page.locator(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
find(byId) {
|
||||||
|
return this.page.locator(`#${byId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findWithParent(byId, withParentId) {
|
||||||
|
return this.page.locator(`#${withParentId} #${byId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findScreenSpecific(byId) {
|
||||||
|
return this.page.locator(`#${this.screenType}-${byId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(byId) {
|
||||||
|
return this.page.locator(`#${byId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllWithTag(tag) {
|
||||||
|
return this.page.locator(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllWithTagFromElement(element, tag) {
|
||||||
|
return element.locator(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
findButtonWithLabel(label) {
|
||||||
|
return this.page.locator(`button[label="${label}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findChildren(ofId, tagname) {
|
||||||
|
return this.page.locator(`#${ofId} ${tagname}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findText(byId) {
|
||||||
|
return (await this.page.locator(`#${byId}`).textContent()) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async findInt(byId) {
|
||||||
|
const text = await this.findText(byId);
|
||||||
|
return parseInt(text, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickSearchBackground() {
|
||||||
|
await this.page.locator('#searchBackground').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickElement(element) {
|
||||||
|
await element.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async enterInput(element, value) {
|
||||||
|
await element.fill(String(value));
|
||||||
|
await element.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto(path) {
|
||||||
|
if (path) {
|
||||||
|
await this.page.goto(`${this.baseUrl}/${path}`);
|
||||||
|
} else {
|
||||||
|
await this.page.goto(this.baseUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Website, ScreenType };
|
||||||
Generated
+76
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"name": "playwright",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "playwright",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@playwright/test": "^1.60.0",
|
||||||
|
"playwright": "^1.60.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||||
|
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.60.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||||
|
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.60.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||||
|
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "playwright",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "npx playwright test",
|
||||||
|
"test:headed": "npx playwright test --headed",
|
||||||
|
"report": "npx playwright show-report"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"@playwright/test": "^1.60.0",
|
||||||
|
"playwright": "^1.60.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
class BasePage {
|
||||||
|
constructor(website) {
|
||||||
|
this.website = website;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url() {
|
||||||
|
throw new Error('Subclasses must implement url');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLinks() {
|
||||||
|
const content = this.website.find('content');
|
||||||
|
const links = content.locator('a');
|
||||||
|
return await links.evaluateAll(els => els.map(el => el.getAttribute('href')).filter(Boolean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BasePage;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
class ArmyComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
armyView() {
|
||||||
|
return this.page.locator('.armyView');
|
||||||
|
}
|
||||||
|
|
||||||
|
displayValue(label) {
|
||||||
|
return this.page.locator('.displayContainer').filter({ hasText: label }).locator('.displayContent');
|
||||||
|
}
|
||||||
|
|
||||||
|
armyCards() {
|
||||||
|
return this.armyView().locator('.armyCard');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getArmyCompletedAt() {
|
||||||
|
return await this.displayValue('Army Completed At').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getArmyAttackingAt() {
|
||||||
|
return await this.displayValue('Army Attacking At').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getArmyUnitNames() {
|
||||||
|
const cards = await this.armyCards().all();
|
||||||
|
const names = [];
|
||||||
|
for (const card of cards) {
|
||||||
|
const text = await card.innerText();
|
||||||
|
const match = text.match(/\d+x\s*(.+)/);
|
||||||
|
names.push(match ? match[1].trim() : text.trim());
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getArmyUnitCounts() {
|
||||||
|
const cards = await this.armyCards().all();
|
||||||
|
const counts = [];
|
||||||
|
for (const card of cards) {
|
||||||
|
const countEl = card.locator('.armyCount');
|
||||||
|
const nameEl = card.locator('div').last();
|
||||||
|
const count = await countEl.textContent();
|
||||||
|
const name = await nameEl.textContent();
|
||||||
|
const num = count ? parseInt(count.replace('x', ''), 10) : 0;
|
||||||
|
counts.push({ name: (name || '').trim(), count: num });
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ArmyComponent;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
class BankComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
bankContainer() {
|
||||||
|
return this.page.locator('.bankContainer');
|
||||||
|
}
|
||||||
|
|
||||||
|
displayValue(label) {
|
||||||
|
return this.bankContainer().locator('.displayContainer').filter({ hasText: label }).locator('.displayContent');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTime() {
|
||||||
|
return await this.displayValue('Time').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAlloy() {
|
||||||
|
return await this.displayValue('Alloy').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEther() {
|
||||||
|
return await this.displayValue('Ether').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPyre() {
|
||||||
|
return await this.displayValue('Pyre').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSupply() {
|
||||||
|
return await this.displayValue('Supply').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkerCount() {
|
||||||
|
return await this.bankContainer().locator('.workerText').locator('.displayContent').nth(0).textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBusyWorkerCount() {
|
||||||
|
return await this.bankContainer().locator('.workerText').locator('.displayContent').nth(1).textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCreatingWorkerCount() {
|
||||||
|
return await this.bankContainer().locator('.workerText').locator('.displayContent').nth(2).textContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BankComponent;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
class BuildChartComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
chartsContainer() {
|
||||||
|
return this.page.locator('.chartsContainer');
|
||||||
|
}
|
||||||
|
|
||||||
|
displayValue(label) {
|
||||||
|
return this.page.locator('.displayContainer').filter({ hasText: label }).locator('.displayContent');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHighestAlloy() {
|
||||||
|
return await this.displayValue('Highest Alloy').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHighestEther() {
|
||||||
|
return await this.displayValue('Highest Ether').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHighestPyre() {
|
||||||
|
return await this.displayValue('Highest Pyre').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHighestArmy() {
|
||||||
|
return await this.displayValue('Highest Army').textContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChartCount() {
|
||||||
|
return await this.chartsContainer().locator('> div').count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BuildChartComponent;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
class BuildOrderComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonTextarea() {
|
||||||
|
return this.page.locator('textarea');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getJsonData() {
|
||||||
|
return await this.jsonTextarea().inputValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BuildOrderComponent;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
class EntityClickViewComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityClickView() {
|
||||||
|
return this.page.locator('.entityClickView');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntityName() {
|
||||||
|
const el = this.entityClickView().locator('#entityName');
|
||||||
|
if ((await el.count()) === 0) return null;
|
||||||
|
return (await el.textContent()) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntityHealth() {
|
||||||
|
const healthText = this.entityClickView().locator('div').filter({ hasText: /Health/i }).first();
|
||||||
|
if ((await healthText.count()) === 0) return null;
|
||||||
|
const text = (await healthText.textContent()) || '';
|
||||||
|
const match = text.match(/(\d+)/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickDetailedView() {
|
||||||
|
await this.entityClickView().locator('button').filter({ hasText: 'Detailed' }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickPlainView() {
|
||||||
|
await this.entityClickView().locator('button').filter({ hasText: 'Plain' }).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EntityClickViewComponent;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
class FilterComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
factionSelect() {
|
||||||
|
return this.page.locator('select').filter({ has: this.page.locator('option:has-text("Aru"), option:has-text("Q\'Rath")') });
|
||||||
|
}
|
||||||
|
|
||||||
|
immortalSelect() {
|
||||||
|
return this.page.locator('select').filter({ has: this.page.locator('option:has-text("Orzum"), option:has-text("Ajari"), option:has-text("Atzlan"), option:has-text("Mala"), option:has-text("Xol")') });
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectFaction(faction) {
|
||||||
|
await this.factionSelect().selectOption(faction);
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectImmortal(immortal) {
|
||||||
|
await this.immortalSelect().selectOption(immortal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSelectedFaction() {
|
||||||
|
return await this.factionSelect().inputValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSelectedImmortal() {
|
||||||
|
return await this.immortalSelect().inputValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAvailableImmortals() {
|
||||||
|
return await this.immortalSelect().locator('option').allTextContents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FilterComponent;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
class HighlightsComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightsContainer() {
|
||||||
|
return this.page.locator('.highlightsContainer');
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedColumn() {
|
||||||
|
return this.highlightsContainer().locator('div').filter({ hasText: 'Requested' }).locator('+ div');
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedColumn() {
|
||||||
|
return this.highlightsContainer().locator('div').filter({ hasText: 'Finished' }).locator('+ div');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRequestedItems() {
|
||||||
|
const items = await this.highlightsContainer().locator('div').filter({ hasText: /^\d+\s*\|/ }).all();
|
||||||
|
const result = [];
|
||||||
|
for (const item of items) {
|
||||||
|
const text = (await item.textContent()) || '';
|
||||||
|
result.push(text.trim());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFinishedItems() {
|
||||||
|
const items = await this.highlightsContainer().locator('div').filter({ hasText: /^\d+\s*\|/ }).all();
|
||||||
|
const result = [];
|
||||||
|
for (const item of items) {
|
||||||
|
const text = (await item.textContent()) || '';
|
||||||
|
result.push(text.trim());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HighlightsComponent;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
class HotkeyViewerComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyContainer() {
|
||||||
|
return this.page.locator('.keyContainer');
|
||||||
|
}
|
||||||
|
|
||||||
|
async _findKeyButton(keyLabel) {
|
||||||
|
const upper = keyLabel.toUpperCase();
|
||||||
|
const buttons = this.keyContainer().locator('> div > div');
|
||||||
|
const count = await buttons.count();
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const btn = buttons.nth(i);
|
||||||
|
const text = (await btn.textContent()) || '';
|
||||||
|
if (text.trim().toUpperCase().startsWith(upper)) return btn;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickKey(keyText) {
|
||||||
|
const btn = await this._findKeyButton(keyText);
|
||||||
|
if (!btn) throw new Error(`Key "${keyText}" not found`);
|
||||||
|
await btn.click({ force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFirstEntityName(keyText) {
|
||||||
|
const btn = await this._findKeyButton(keyText);
|
||||||
|
if (!btn) return null;
|
||||||
|
const entities = btn.locator('> div');
|
||||||
|
if ((await entities.count()) === 0) return null;
|
||||||
|
return (await entities.first().textContent()) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntityNamesOnKey(keyText) {
|
||||||
|
const btn = await this._findKeyButton(keyText);
|
||||||
|
if (!btn) return [];
|
||||||
|
const entities = btn.locator('> div');
|
||||||
|
const count = await entities.count();
|
||||||
|
const names = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const text = (await entities.nth(i).textContent()) || '';
|
||||||
|
names.push(text.trim());
|
||||||
|
}
|
||||||
|
return names.filter(Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HotkeyViewerComponent;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
class OptionsComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildingInputDelayInput() {
|
||||||
|
return this.formNumberInput('Building Input Delay');
|
||||||
|
}
|
||||||
|
|
||||||
|
waitTimeInput() {
|
||||||
|
return this.formNumberInput('Wait Time');
|
||||||
|
}
|
||||||
|
|
||||||
|
waitToInput() {
|
||||||
|
return this.formNumberInput('Wait To');
|
||||||
|
}
|
||||||
|
|
||||||
|
addWaitButton() {
|
||||||
|
return this.buttonWithLabel('Add Wait').first();
|
||||||
|
}
|
||||||
|
|
||||||
|
addWaitToButton() {
|
||||||
|
return this.buttonWithLabel('Add Wait').last();
|
||||||
|
}
|
||||||
|
|
||||||
|
formNumberInput(label) {
|
||||||
|
return this.page.locator(`.formNumberContainer`).filter({ hasText: label }).locator('input[type="number"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonWithLabel(label) {
|
||||||
|
return this.page.locator('button').filter({ hasText: label });
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBuildingInputDelay(value) {
|
||||||
|
await this.buildingInputDelayInput().fill(String(value));
|
||||||
|
await this.buildingInputDelayInput().press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
async setWaitTime(value) {
|
||||||
|
await this.waitTimeInput().fill(String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
async setWaitTo(value) {
|
||||||
|
await this.waitToInput().fill(String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickAddWait() {
|
||||||
|
await this.addWaitButton().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickAddWaitTo() {
|
||||||
|
await this.addWaitToButton().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBuildingInputDelay() {
|
||||||
|
return await this.buildingInputDelayInput().inputValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWaitTime() {
|
||||||
|
return await this.waitTimeInput().inputValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWaitTo() {
|
||||||
|
return await this.waitToInput().inputValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = OptionsComponent;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
class TimelineComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
container() {
|
||||||
|
return this.page.locator('.calculatorGrid > div').filter({ hasText: 'Timeline highlights' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async containsEntity(name) {
|
||||||
|
const text = (await this.container().textContent()) || '';
|
||||||
|
return text.includes(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TimelineComponent;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
class TimingComponent {
|
||||||
|
constructor(website) {
|
||||||
|
this.website = website;
|
||||||
|
}
|
||||||
|
|
||||||
|
attackTimeInput() {
|
||||||
|
return this.formNumberInput('Attack Time');
|
||||||
|
}
|
||||||
|
|
||||||
|
travelTimeInput() {
|
||||||
|
return this.formNumberInput('Travel Time');
|
||||||
|
}
|
||||||
|
|
||||||
|
formNumberInput(label) {
|
||||||
|
return this.website.locator(`.formNumberContainer`).filter({ hasText: label }).locator('input[type="number"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAttackTime(value) {
|
||||||
|
await this.attackTimeInput().fill(String(value));
|
||||||
|
await this.attackTimeInput().press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTravelTime(value) {
|
||||||
|
await this.travelTimeInput().fill(String(value));
|
||||||
|
await this.travelTimeInput().press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAttackTime() {
|
||||||
|
return await this.attackTimeInput().inputValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTravelTime() {
|
||||||
|
return await this.travelTimeInput().inputValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TimingComponent;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const TimingComponent = require('./buildCalculator/timingComponent');
|
||||||
|
const FilterComponent = require('./buildCalculator/filterComponent');
|
||||||
|
const OptionsComponent = require('./buildCalculator/optionsComponent');
|
||||||
|
const BankComponent = require('./buildCalculator/bankComponent');
|
||||||
|
const ArmyComponent = require('./buildCalculator/armyComponent');
|
||||||
|
const HighlightsComponent = require('./buildCalculator/highlightsComponent');
|
||||||
|
const BuildOrderComponent = require('./buildCalculator/buildOrderComponent');
|
||||||
|
const TimelineComponent = require('./buildCalculator/timelineComponent');
|
||||||
|
const HotkeyViewerComponent = require('./buildCalculator/hotkeyViewerComponent');
|
||||||
|
const EntityClickViewComponent = require('./buildCalculator/entityClickViewComponent');
|
||||||
|
const BuildChartComponent = require('./buildCalculator/buildChartComponent');
|
||||||
|
const ToastComponent = require('../shared/toastComponent');
|
||||||
|
|
||||||
|
const BasePage = require('./base.page');
|
||||||
|
|
||||||
|
|
||||||
|
class BuildCalculatorPage extends BasePage {
|
||||||
|
constructor(website) {
|
||||||
|
super(website);
|
||||||
|
this.timing = new TimingComponent(website);
|
||||||
|
this.filter = new FilterComponent(website);
|
||||||
|
this.options = new OptionsComponent(website);
|
||||||
|
this.bank = new BankComponent(website);
|
||||||
|
this.army = new ArmyComponent(website);
|
||||||
|
this.highlights = new HighlightsComponent(website);
|
||||||
|
this.buildOrder = new BuildOrderComponent(website);
|
||||||
|
this.timeline = new TimelineComponent(website);
|
||||||
|
this.hotkeys = new HotkeyViewerComponent(website);
|
||||||
|
this.entityView = new EntityClickViewComponent(website);
|
||||||
|
this.chart = new BuildChartComponent(website);
|
||||||
|
this.toast = new ToastComponent(website);
|
||||||
|
}
|
||||||
|
|
||||||
|
get url() {
|
||||||
|
return 'build-calculator';
|
||||||
|
}
|
||||||
|
|
||||||
|
calculatorGrid() {
|
||||||
|
return this.website.locator('.calculatorGrid');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBuildOrderButton() {
|
||||||
|
return this.website.locator('button').filter({ hasText: 'Clear Build Order' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickClearBuildOrder() {
|
||||||
|
await this.clearBuildOrderButton().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto() {
|
||||||
|
await this.website.goto(this.url);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BuildCalculatorPage;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const BasePage = require('./base.page');
|
||||||
|
|
||||||
|
class DatabasePage extends BasePage {
|
||||||
|
get url() { return 'database'; }
|
||||||
|
|
||||||
|
async filterName(name) {
|
||||||
|
await this.website.enterInput(this.website.findAll('filterName').first(), name);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntityName(entityType, entityName) {
|
||||||
|
return await this.website
|
||||||
|
.findWithParent('entityName', `${entityType.toLowerCase()}-${entityName.toLowerCase()}`)
|
||||||
|
.innerText();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntityNameByIndex(index) {
|
||||||
|
return await this.website.findAll('entityName').nth(index).innerText();
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto() {
|
||||||
|
await this.website.goto(this.url);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DatabasePage;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
const BasePage = require('./base.page');
|
||||||
|
|
||||||
|
class DatabaseSinglePage extends BasePage {
|
||||||
|
get url() { return 'database'; }
|
||||||
|
|
||||||
|
async getEntityName() {
|
||||||
|
return await this.website.find('entityName').innerText();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntityHealth() {
|
||||||
|
return await this.website.find('entityHealth').innerText();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInvalidSearch() {
|
||||||
|
return await this.website.find('invalidSearch').innerText();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getValidSearch() {
|
||||||
|
return await this.website.find('validSearch').innerText();
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto(searchText) {
|
||||||
|
await this.website.goto(`${this.url}/${searchText}`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DatabaseSinglePage;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const BasePage = require('./base.page');
|
||||||
|
|
||||||
|
class HarassCalculatorPage extends BasePage {
|
||||||
|
get url() { return 'harass-calculator'; }
|
||||||
|
|
||||||
|
async setWorkersLostToHarass(number) {
|
||||||
|
await this.website.enterInput(this.website.find('numberOfWorkersLostToHarass'), number);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setNumberOfTownHallsExisting(number) {
|
||||||
|
await this.website.enterInput(this.website.find('numberOfTownHallsExisting'), number);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTownHallTravelTime(forTownHall, number) {
|
||||||
|
const inputs = this.website.findChildren('numberOfTownHallTravelTimes', 'input');
|
||||||
|
await this.website.enterInput(inputs.nth(forTownHall), number);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTotalAlloyHarassment() {
|
||||||
|
return await this.website.findInt('totalAlloyHarassment');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkerReplacementCost() {
|
||||||
|
return await this.website.findInt('workerReplacementCost');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDelayedMiningCost() {
|
||||||
|
return await this.website.findInt('delayedMiningCost');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAverageTravelTime() {
|
||||||
|
return await this.website.findInt('getAverageTravelTime');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExampleTotalAlloyLoss() {
|
||||||
|
return await this.website.findInt('exampleTotalAlloyLoss');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExampleWorkerCost() {
|
||||||
|
return await this.website.findInt('exampleWorkerCost');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExampleMiningTimeCost() {
|
||||||
|
return await this.website.findInt('exampleMiningTimeCost');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExampleTotalAlloyLossAccurate() {
|
||||||
|
return await this.website.findInt('exampleTotalAlloyLossAccurate');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExampleTotalAlloyLossDifference() {
|
||||||
|
return await this.website.findInt('exampleTotalAlloyLossDifference');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExampleTotalAlloyLossAccurateDifference() {
|
||||||
|
return await this.website.findInt('exampleTotalAlloyLossAccurateDifference');
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto() {
|
||||||
|
await this.website.goto(this.url);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HarassCalculatorPage;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const { defineConfig } = require('@playwright/test');
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
fullyParallel: true,
|
||||||
|
retries: 1,
|
||||||
|
timeout: 30000,
|
||||||
|
use: {
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: 'chromium', use: { browserName: 'chromium' } },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
class NavigationBar {
|
||||||
|
constructor(website) {
|
||||||
|
this.website = website;
|
||||||
|
}
|
||||||
|
|
||||||
|
get searchButton() { return this.website.findScreenSpecific('searchButton'); }
|
||||||
|
|
||||||
|
async clickHomeLink() {
|
||||||
|
await this.website.clickElement(this.website.locator('a:has-text("IGP Fan Reference")'));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickSearchButton() {
|
||||||
|
await this.website.clickElement(this.searchButton);
|
||||||
|
return this.website.websiteSearchDialog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = NavigationBar;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
class ToastComponent {
|
||||||
|
constructor(page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
container() {
|
||||||
|
return this.page.locator('.toastsContainer');
|
||||||
|
}
|
||||||
|
|
||||||
|
toasts() {
|
||||||
|
return this.page.locator('.toastsContainer .toastContainer');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getToastTitles() {
|
||||||
|
const titles = await this.page.locator('.toastsContainer .toastTitle').allTextContents();
|
||||||
|
return titles.map(t => t.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
_page() {
|
||||||
|
return this.page.page || this.page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasToastContaining(text) {
|
||||||
|
try {
|
||||||
|
await this._page().waitForFunction(
|
||||||
|
(expected) => {
|
||||||
|
const titles = document.querySelectorAll('.toastsContainer .toastTitle');
|
||||||
|
return Array.from(titles).some(t => t.textContent.trim().includes(expected));
|
||||||
|
},
|
||||||
|
text,
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ToastComponent;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
class WebsiteSearchDialog {
|
||||||
|
constructor(website) {
|
||||||
|
this.website = website;
|
||||||
|
}
|
||||||
|
|
||||||
|
get searchBackground() { return this.website.find('searchBackground'); }
|
||||||
|
get searchInput() { return this.website.find('searchInput'); }
|
||||||
|
|
||||||
|
async closeDialog() {
|
||||||
|
await this.website.clickSearchBackground();
|
||||||
|
return this.website.navigationBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(text) {
|
||||||
|
await this.website.enterInput(this.searchInput, text);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectSearchEntity(label) {
|
||||||
|
await this.website.clickElement(this.website.findButtonWithLabel(label));
|
||||||
|
return this.website.databaseSinglePage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WebsiteSearchDialog;
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
const BuildCalculatorPage = require('../pages/buildCalculatorPage');
|
||||||
|
const { Website } = require('../helpers/website');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test.describe('Build Calculator', () => {
|
||||||
|
|
||||||
|
let website;
|
||||||
|
|
||||||
|
test.beforeEach(({ page }) => {
|
||||||
|
website = new Website(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add entities via keyboard Q, W, E with Q\'Rath/Orzum', async ({ page }) => {
|
||||||
|
const calc = website.buildCalculatorPage;
|
||||||
|
await calc.goto();
|
||||||
|
|
||||||
|
await calc.filter.selectFaction("Q'Rath");
|
||||||
|
await calc.filter.selectImmortal('Orzum');
|
||||||
|
|
||||||
|
await calc.hotkeys.clickKey('TAB');
|
||||||
|
|
||||||
|
const keyNames = { Q: 'q', W: 'w', E: 'e', TAB: 'Tab' };
|
||||||
|
|
||||||
|
for (const key of ['Q', 'W', 'E', 'TAB']) {
|
||||||
|
const entityNames = await calc.hotkeys.getEntityNamesOnKey(key);
|
||||||
|
if (entityNames.length === 0) continue;
|
||||||
|
|
||||||
|
await page.keyboard.press(keyNames[key]);
|
||||||
|
|
||||||
|
const viewName = await calc.entityView.getEntityName();
|
||||||
|
expect(viewName).toBeTruthy();
|
||||||
|
expect(entityNames).toContain(viewName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add entities via hotkeys TAB, Q, W, E with Q\'Rath/Orzum', async ({ page }) => {
|
||||||
|
const calc = website.buildCalculatorPage;
|
||||||
|
await calc.goto();
|
||||||
|
|
||||||
|
await calc.filter.selectFaction("Q'Rath");
|
||||||
|
await calc.filter.selectImmortal('Orzum');
|
||||||
|
|
||||||
|
for (const key of ['TAB', 'Q', 'W', 'E']) {
|
||||||
|
const entityNames = await calc.hotkeys.getEntityNamesOnKey(key);
|
||||||
|
if (entityNames.length === 0) continue;
|
||||||
|
|
||||||
|
await calc.hotkeys.clickKey(key);
|
||||||
|
|
||||||
|
const viewName = await calc.entityView.getEntityName();
|
||||||
|
expect(viewName).toBeTruthy();
|
||||||
|
expect(entityNames).toContain(viewName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add Acropolis via Q, verify entity view and timeline, then clear', async ({ page }) => {
|
||||||
|
const calc = website.buildCalculatorPage;
|
||||||
|
await calc.goto();
|
||||||
|
|
||||||
|
await calc.filter.selectFaction("Q'Rath");
|
||||||
|
await calc.filter.selectImmortal('Orzum');
|
||||||
|
|
||||||
|
expect(await calc.timeline.containsEntity('Acropolis')).toBe(false);
|
||||||
|
|
||||||
|
await calc.hotkeys.clickKey('Q');
|
||||||
|
|
||||||
|
expect(await calc.entityView.getEntityName()).toBe('Acropolis');
|
||||||
|
expect(await calc.timeline.containsEntity('Acropolis')).toBe(true);
|
||||||
|
|
||||||
|
await calc.clickClearBuildOrder();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
expect(await calc.timeline.containsEntity('Acropolis')).toBe(false);
|
||||||
|
expect(await calc.entityView.getEntityName()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Missing Requirements toast when building Soul Foundry without Legion Hall', async ({ page }) => {
|
||||||
|
const calc = website.buildCalculatorPage;
|
||||||
|
await calc.goto();
|
||||||
|
|
||||||
|
await calc.filter.selectFaction("Q'Rath");
|
||||||
|
await calc.filter.selectImmortal('Orzum');
|
||||||
|
|
||||||
|
await calc.hotkeys.clickKey('E');
|
||||||
|
const hasToast = await calc.toast.hasToastContaining('Missing Requirements');
|
||||||
|
expect(hasToast).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Not Enough Ether toast when building Soul Foundry after Legion Hall', async ({ page }) => {
|
||||||
|
const calc = website.buildCalculatorPage;
|
||||||
|
|
||||||
|
await calc.goto();
|
||||||
|
|
||||||
|
await calc.filter.selectFaction("Q'Rath");
|
||||||
|
await calc.filter.selectImmortal('Orzum');
|
||||||
|
|
||||||
|
await calc.hotkeys.clickKey('W');
|
||||||
|
|
||||||
|
await calc.hotkeys.clickKey('E');
|
||||||
|
const hasToast = await calc.toast.hasToastContaining('Not Enough Ether');
|
||||||
|
expect(hasToast).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
const { Website } = require('../helpers/website');
|
||||||
|
|
||||||
|
test.describe('Harass Calculator', () => {
|
||||||
|
let website;
|
||||||
|
|
||||||
|
test.beforeEach(({ page }) => {
|
||||||
|
website = new Website(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CalculatorInput', async () => {
|
||||||
|
const page = website.harassCalculatorPage;
|
||||||
|
await page.goto();
|
||||||
|
await page.setWorkersLostToHarass(3);
|
||||||
|
await page.setNumberOfTownHallsExisting(2);
|
||||||
|
await page.setTownHallTravelTime(0, 30);
|
||||||
|
const result = await page.getTotalAlloyHarassment();
|
||||||
|
expect(result).toBe(240);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CalculatedExampleInformation', async () => {
|
||||||
|
const page = website.harassCalculatorPage;
|
||||||
|
await page.goto();
|
||||||
|
|
||||||
|
expect(await page.getExampleTotalAlloyLoss()).toBe(720);
|
||||||
|
expect(await page.getExampleWorkerCost()).toBe(300);
|
||||||
|
expect(await page.getExampleMiningTimeCost()).toBe(420);
|
||||||
|
expect(await page.getExampleTotalAlloyLossAccurate()).toBe(450);
|
||||||
|
expect(await page.getExampleTotalAlloyLossDifference()).toBe(300);
|
||||||
|
expect(await page.getExampleTotalAlloyLossAccurateDifference()).toBe(270);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
const { test } = require('@playwright/test');
|
||||||
|
const { Website } = require('../helpers/website');
|
||||||
|
const TestReport = require('../utils/testReport');
|
||||||
|
|
||||||
|
test.describe('Link Verification', () => {
|
||||||
|
let website;
|
||||||
|
let testReport;
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
testReport = new TestReport();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VerifyPageLinks', async ({ page }) => {
|
||||||
|
website = new Website(page);
|
||||||
|
testReport.createTest(test.info().title);
|
||||||
|
|
||||||
|
await website.harassCalculatorPage.goto();
|
||||||
|
await testReport.verifyLinks(website.harassCalculatorPage);
|
||||||
|
|
||||||
|
await website.databasePage.goto();
|
||||||
|
await testReport.verifyLinks(website.databasePage);
|
||||||
|
|
||||||
|
await website.databaseSinglePage.goto('throne');
|
||||||
|
await testReport.verifyLinks(website.databaseSinglePage);
|
||||||
|
|
||||||
|
testReport.throwErrors();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
const { Website } = require('../helpers/website');
|
||||||
|
|
||||||
|
test.describe('Search Features', () => {
|
||||||
|
let website;
|
||||||
|
|
||||||
|
test.beforeEach(({ page }) => {
|
||||||
|
website = new Website(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DesktopOpenCloseSearchDialog', async () => {
|
||||||
|
await website.goto();
|
||||||
|
await website.navigationBar.clickSearchButton();
|
||||||
|
await website.websiteSearchDialog.closeDialog();
|
||||||
|
await website.navigationBar.clickHomeLink();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DesktopSearchForThrone', async () => {
|
||||||
|
await website.goto();
|
||||||
|
await website.navigationBar.clickSearchButton();
|
||||||
|
await website.websiteSearchDialog.search('Throne');
|
||||||
|
const page = await website.websiteSearchDialog.selectSearchEntity('Throne');
|
||||||
|
|
||||||
|
const name = await page.getEntityName();
|
||||||
|
const health = await page.getEntityHealth();
|
||||||
|
|
||||||
|
expect(name).toBe('Throne');
|
||||||
|
expect(health.trim()).not.toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DesktopFilterForThrone', async () => {
|
||||||
|
const page = website.databasePage;
|
||||||
|
await page.goto();
|
||||||
|
await page.filterName('Throne');
|
||||||
|
const name = await page.getEntityNameByIndex(0);
|
||||||
|
expect(name).toBe('Throne');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SeeThroneByDefault', async () => {
|
||||||
|
const page = website.databasePage;
|
||||||
|
await page.goto();
|
||||||
|
const name = await page.getEntityName('army', 'throne');
|
||||||
|
expect(name).toBe('Throne');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DirectLinkNotThroneFailure', async () => {
|
||||||
|
const page = website.databaseSinglePage;
|
||||||
|
await page.goto('not throne');
|
||||||
|
const invalidSearch = await page.getInvalidSearch();
|
||||||
|
const validSearch = await page.getValidSearch();
|
||||||
|
expect(invalidSearch).toBe('not throne');
|
||||||
|
expect(validSearch).toBe('Throne');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
class TestReport {
|
||||||
|
constructor() {
|
||||||
|
this.tests = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
createTest(name) {
|
||||||
|
const test = { name, result: true, messages: [] };
|
||||||
|
this.tests.push(test);
|
||||||
|
return test;
|
||||||
|
}
|
||||||
|
|
||||||
|
throwErrors() {
|
||||||
|
const latest = this.tests[this.tests.length - 1];
|
||||||
|
if (!latest.result) {
|
||||||
|
const msgs = latest.messages.map(m => m.description).join('\n');
|
||||||
|
throw new Error(`${latest.name} test failed with ${latest.messages.length} messages.\n\n${msgs}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPassed(passed, message) {
|
||||||
|
if (!passed) {
|
||||||
|
const latest = this.tests[this.tests.length - 1];
|
||||||
|
latest.result = false;
|
||||||
|
latest.messages.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyLinks(page) {
|
||||||
|
const links = await page.getLinks();
|
||||||
|
for (const link of links) {
|
||||||
|
if (link.startsWith('mailto')) continue;
|
||||||
|
try {
|
||||||
|
const response = await fetch(link);
|
||||||
|
if (!response.ok) {
|
||||||
|
this.checkPassed(false, {
|
||||||
|
color: 'red',
|
||||||
|
title: 'Bad Link',
|
||||||
|
description: `${link} failed on page ${page.url} with status code ${response.status}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.checkPassed(false, {
|
||||||
|
color: 'red',
|
||||||
|
title: 'Bad Link',
|
||||||
|
description: `${link} failed on page ${page.url} with error ${e.message}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
didTestsPass() {
|
||||||
|
return this.tests.every(t => t.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMessages() {
|
||||||
|
if (this.didTestsPass()) {
|
||||||
|
return [{
|
||||||
|
title: 'Passed',
|
||||||
|
color: 0x00FF00,
|
||||||
|
description: `All ${this.tests.length} tests passed.`
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
const messages = [];
|
||||||
|
for (const test of this.tests) {
|
||||||
|
for (const msg of test.messages) {
|
||||||
|
messages.push({
|
||||||
|
title: msg.title,
|
||||||
|
color: parseInt(msg.color, 16),
|
||||||
|
description: msg.description
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TestReport;
|
||||||
@@ -1 +1,26 @@
|
|||||||
|
|
||||||
|
|
||||||
|
# IGP Fan Reference
|
||||||
|
|
||||||
|
A fan-made reference site for *IMMORTAL: Gates of Pyre*.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Comprehensive documentation for developers is available in the [`/docs`](./docs) folder:
|
||||||
|
|
||||||
|
- [**Overview**](./docs/overview.md): High-level project description and tech stack.
|
||||||
|
- [**Architecture**](./docs/architecture.md): Solution structure and design patterns.
|
||||||
|
- [**Services**](./docs/services.md): Detailed explanation of core application services.
|
||||||
|
- [**Components**](./docs/components.md): UI component library and layout structure.
|
||||||
|
- [**Development Guide**](./docs/development.md): Build, test, and deployment instructions.
|
||||||
|
- [**Recommendations**](./docs/recommendations.md): Suggestions for code and design improvements.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
To run the project locally:
|
||||||
|
|
||||||
|
1. Navigate to the `IGP` directory.
|
||||||
|
2. Run `dotnet watch run`.
|
||||||
|
3. Open `https://localhost:5001`.
|
||||||
|
|
||||||
|
For more details, see the [Development Guide](./docs/development.md).
|
||||||
|
|||||||
@@ -291,6 +291,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();
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ public class BuildOrderService : IBuildOrderService
|
|||||||
{
|
{
|
||||||
return (from ordersAtTime in _buildOrder.StartedOrders
|
return (from ordersAtTime in _buildOrder.StartedOrders
|
||||||
from orders in ordersAtTime.Value
|
from orders in ordersAtTime.Value
|
||||||
|
where orders.Harvest() != null
|
||||||
where ordersAtTime.Key + (orders.Production() == null
|
where ordersAtTime.Key + (orders.Production() == null
|
||||||
? 0
|
? 0
|
||||||
: orders.Production().BuildTime) <= interval
|
: orders.Production().BuildTime) <= interval
|
||||||
@@ -298,7 +299,6 @@ public class BuildOrderService : IBuildOrderService
|
|||||||
ordersAtTime.Key + (orders.Production() == null
|
ordersAtTime.Key + (orders.Production() == null
|
||||||
? 0
|
? 0
|
||||||
: orders.Production().BuildTime))
|
: orders.Production().BuildTime))
|
||||||
where orders.Harvest() != null
|
|
||||||
select orders).ToList();
|
select orders).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<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>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace TestAutomation.Enums;
|
|
||||||
|
|
||||||
public enum ScreenType
|
|
||||||
{
|
|
||||||
Desktop,
|
|
||||||
Tablet,
|
|
||||||
Mobile
|
|
||||||
}
|
|
||||||
@@ -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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using TestAutomation.Utils;
|
|
||||||
|
|
||||||
namespace TestAutomation.Shared;
|
|
||||||
|
|
||||||
public abstract class BaseElement
|
|
||||||
{
|
|
||||||
protected readonly Website Website;
|
|
||||||
|
|
||||||
protected BaseElement(Website website)
|
|
||||||
{
|
|
||||||
Website = website;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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} "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>();
|
|
||||||
}
|
|
||||||
@@ -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" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"theme": "obsidian"
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
"kanban-bases-view",
|
||||||
|
"code-styler",
|
||||||
|
"calendar",
|
||||||
|
"custom-font-loader",
|
||||||
|
"obsidian-note-autocreator"
|
||||||
|
]
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"file-explorer": true,
|
||||||
|
"global-search": true,
|
||||||
|
"switcher": true,
|
||||||
|
"graph": true,
|
||||||
|
"backlink": true,
|
||||||
|
"canvas": true,
|
||||||
|
"outgoing-link": true,
|
||||||
|
"tag-pane": true,
|
||||||
|
"footnotes": false,
|
||||||
|
"properties": true,
|
||||||
|
"page-preview": true,
|
||||||
|
"daily-notes": true,
|
||||||
|
"templates": true,
|
||||||
|
"note-composer": true,
|
||||||
|
"command-palette": true,
|
||||||
|
"slash-command": false,
|
||||||
|
"editor-status": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"markdown-importer": false,
|
||||||
|
"zk-prefixer": false,
|
||||||
|
"random-note": false,
|
||||||
|
"outline": true,
|
||||||
|
"word-count": true,
|
||||||
|
"slides": false,
|
||||||
|
"audio-recorder": false,
|
||||||
|
"workspaces": false,
|
||||||
|
"file-recovery": true,
|
||||||
|
"publish": false,
|
||||||
|
"sync": true,
|
||||||
|
"bases": true,
|
||||||
|
"webviewer": false
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"shouldConfirmBeforeCreate": true,
|
||||||
|
"weekStart": "locale",
|
||||||
|
"wordsPerDot": 250,
|
||||||
|
"showWeeklyNote": false,
|
||||||
|
"weeklyNoteFormat": "",
|
||||||
|
"weeklyNoteTemplate": "",
|
||||||
|
"weeklyNoteFolder": "",
|
||||||
|
"localeOverride": "system-default"
|
||||||
|
}
|
||||||
+4459
File diff suppressed because it is too large
Load Diff
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "calendar",
|
||||||
|
"name": "Calendar",
|
||||||
|
"description": "Calendar view of your daily notes",
|
||||||
|
"version": "1.5.10",
|
||||||
|
"author": "Liam Cain",
|
||||||
|
"authorUrl": "https://github.com/liamcain/",
|
||||||
|
"isDesktopOnly": false,
|
||||||
|
"minAppVersion": "0.9.11"
|
||||||
|
}
|
||||||
+20047
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "code-styler",
|
||||||
|
"name": "Code Styler",
|
||||||
|
"version": "1.1.7",
|
||||||
|
"minAppVersion": "0.15.0",
|
||||||
|
"description": "Style and customize codeblocks and inline code in both editing mode and reading mode.",
|
||||||
|
"author": "Mayuran Visakan",
|
||||||
|
"authorUrl": "https://github.com/mayurankv",
|
||||||
|
"fundingUrl": "https://www.buymeacoffee.com/mayurankv2",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
+1348
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"font_folder": ".obsidian/fonts/",
|
||||||
|
"font": "JetBrainsMono-Regular.ttf",
|
||||||
|
"force_mode": false,
|
||||||
|
"custom_css_mode": false,
|
||||||
|
"custom_css": ""
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
+357
@@ -0,0 +1,357 @@
|
|||||||
|
/*
|
||||||
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
|
||||||
|
// main.ts
|
||||||
|
var main_exports = {};
|
||||||
|
__export(main_exports, {
|
||||||
|
default: () => FontPlugin
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(main_exports);
|
||||||
|
var import_obsidian = require("obsidian");
|
||||||
|
var DEFAULT_SETTINGS = {
|
||||||
|
font_folder: "",
|
||||||
|
font: "None",
|
||||||
|
force_mode: false,
|
||||||
|
custom_css_mode: false,
|
||||||
|
custom_css: ""
|
||||||
|
};
|
||||||
|
function get_default_css(font_family_name, css_class = ":root *") {
|
||||||
|
return `${css_class} {
|
||||||
|
--font-default: '${font_family_name}';
|
||||||
|
--default-font: '${font_family_name}';
|
||||||
|
--font-family-editor: '${font_family_name}';
|
||||||
|
--font-monospace-default: '${font_family_name}';
|
||||||
|
--font-interface-override: '${font_family_name}';
|
||||||
|
--font-text-override: '${font_family_name}';
|
||||||
|
--font-monospace-override: '${font_family_name}';
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
function get_custom_css(font_family_name, css_class = ":root *") {
|
||||||
|
return `${css_class} * {
|
||||||
|
font-family: '${font_family_name}' !important;
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
function arrayBufferToBase64(buffer) {
|
||||||
|
let binary = "";
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
for (let i = 0; i < bytes.byteLength; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
function applyCss(css, css_id, appendMode = false) {
|
||||||
|
const existingStyle = document.getElementById(css_id);
|
||||||
|
if (existingStyle && appendMode) {
|
||||||
|
existingStyle.innerHTML += css;
|
||||||
|
} else {
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.innerHTML = css;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
if (existingStyle) {
|
||||||
|
existingStyle.remove();
|
||||||
|
}
|
||||||
|
style.id = css_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var FontPlugin = class extends import_obsidian.Plugin {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.config_dir = this.app.vault.configDir;
|
||||||
|
this.plugin_folder_path = `${this.config_dir}/plugins/custom-font-loader`;
|
||||||
|
}
|
||||||
|
async load_plugin() {
|
||||||
|
await this.loadSettings();
|
||||||
|
try {
|
||||||
|
const font_file_name = this.settings.font;
|
||||||
|
if (font_file_name && font_file_name.toLowerCase() != "none") {
|
||||||
|
if (font_file_name != "all") {
|
||||||
|
await this.process_and_load_font(font_file_name, false);
|
||||||
|
} else {
|
||||||
|
applyCss("", "custom_font_base64");
|
||||||
|
const files = await this.app.vault.adapter.list(
|
||||||
|
this.settings.font_folder
|
||||||
|
);
|
||||||
|
for (const file of files.files) {
|
||||||
|
const file_name = file.replace(
|
||||||
|
this.settings.font_folder,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
await this.process_and_load_font(file_name, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
applyCss("", "custom_font_base64");
|
||||||
|
applyCss("", "custom_font_general");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
new import_obsidian.Notice(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async process_and_load_font(font_file_name, load_all_fonts) {
|
||||||
|
console.log("loading %s", font_file_name);
|
||||||
|
const css_font_path = `${this.plugin_folder_path}/${font_file_name.toLowerCase().replace(".", "_")}.css`;
|
||||||
|
if (!await this.app.vault.adapter.exists(css_font_path)) {
|
||||||
|
await this.convert_font_to_css(font_file_name, css_font_path);
|
||||||
|
} else {
|
||||||
|
await this.load_font(css_font_path, load_all_fonts);
|
||||||
|
await this.load_css(font_file_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async load_font(css_font_path, appendMode) {
|
||||||
|
const content = await this.app.vault.adapter.read(css_font_path);
|
||||||
|
applyCss(content, "custom_font_base64", appendMode);
|
||||||
|
}
|
||||||
|
async load_css(font_file_name) {
|
||||||
|
let css_string = "";
|
||||||
|
const font_family_name = font_file_name.split(".")[0].toLowerCase();
|
||||||
|
if (this.settings.custom_css_mode) {
|
||||||
|
css_string = this.settings.custom_css;
|
||||||
|
} else {
|
||||||
|
css_string = get_default_css(font_family_name);
|
||||||
|
}
|
||||||
|
if (this.settings.force_mode)
|
||||||
|
css_string += `
|
||||||
|
* {
|
||||||
|
font-family: '${font_family_name}' !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
applyCss(css_string, "custom_font_general");
|
||||||
|
}
|
||||||
|
async convert_font_to_css(font_file_name, css_font_path) {
|
||||||
|
new import_obsidian.Notice("Processing Font files");
|
||||||
|
const file = `${this.settings.font_folder}/${font_file_name}`;
|
||||||
|
const arrayBuffer = await this.app.vault.adapter.readBinary(file);
|
||||||
|
const base64 = arrayBufferToBase64(arrayBuffer);
|
||||||
|
const font_family_name = font_file_name.split(".")[0].toLowerCase();
|
||||||
|
const font_extension_name = font_file_name.split(".")[1].toLowerCase();
|
||||||
|
let css_type = "";
|
||||||
|
switch (font_extension_name) {
|
||||||
|
case "woff":
|
||||||
|
css_type = "font/woff";
|
||||||
|
break;
|
||||||
|
case "ttf":
|
||||||
|
css_type = "font/truetype";
|
||||||
|
break;
|
||||||
|
case "woff2":
|
||||||
|
css_type = "font/woff2";
|
||||||
|
break;
|
||||||
|
case "otf":
|
||||||
|
css_type = "font/opentype";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
css_type = "font";
|
||||||
|
}
|
||||||
|
const base64_css = `@font-face{
|
||||||
|
font-family: '${font_family_name}';
|
||||||
|
src: url(data:${css_type};base64,${base64});
|
||||||
|
}`;
|
||||||
|
this.app.vault.adapter.write(css_font_path, base64_css);
|
||||||
|
console.log("saved font %s into %s", font_family_name, css_font_path);
|
||||||
|
console.log("Font CSS Saved into %s", css_font_path);
|
||||||
|
await this.load_plugin();
|
||||||
|
}
|
||||||
|
async onload() {
|
||||||
|
this.load_plugin();
|
||||||
|
this.addSettingTab(new FontSettingTab(this.app, this));
|
||||||
|
}
|
||||||
|
async onunload() {
|
||||||
|
applyCss("", "custom_font_base64");
|
||||||
|
applyCss("", "custom_font_general");
|
||||||
|
}
|
||||||
|
async loadSettings() {
|
||||||
|
this.settings = Object.assign(
|
||||||
|
{},
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
await this.loadData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async saveSettings() {
|
||||||
|
await this.saveData(this.settings);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var FontSettingTab = class extends import_obsidian.PluginSettingTab {
|
||||||
|
constructor(app, plugin) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
async display() {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
const infoContainer = containerEl.createDiv();
|
||||||
|
infoContainer.setText(
|
||||||
|
"In Order to set the font, copy your font into fonts directory that you set"
|
||||||
|
);
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Fonts Folder").setDesc("Folder to look for your custom fonts").addText((text) => {
|
||||||
|
text.onChange(async (value) => {
|
||||||
|
this.plugin.settings.font_folder = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
await this.plugin.loadSettings();
|
||||||
|
});
|
||||||
|
if (this.plugin.settings.font_folder.trim() == "") {
|
||||||
|
this.plugin.settings.font_folder = `${this.app.vault.configDir}/fonts`;
|
||||||
|
}
|
||||||
|
if (!this.plugin.settings.font_folder.endsWith("/"))
|
||||||
|
this.plugin.settings.font_folder = this.plugin.settings.font_folder + "/";
|
||||||
|
text.setValue(this.plugin.settings.font_folder);
|
||||||
|
});
|
||||||
|
const font_folder_path = this.plugin.settings.font_folder;
|
||||||
|
const options = [{ name: "none", value: "None" }];
|
||||||
|
try {
|
||||||
|
if (!await this.app.vault.adapter.exists(font_folder_path)) {
|
||||||
|
await this.app.vault.adapter.mkdir(font_folder_path);
|
||||||
|
}
|
||||||
|
if (await this.app.vault.adapter.exists(font_folder_path)) {
|
||||||
|
const files = await this.app.vault.adapter.list(
|
||||||
|
font_folder_path
|
||||||
|
);
|
||||||
|
for (const file of files.files) {
|
||||||
|
const file_name = file.replace(font_folder_path, "");
|
||||||
|
if (file_name.startsWith("."))
|
||||||
|
continue;
|
||||||
|
options.push({ name: file_name, value: file_name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.push({ name: "all", value: "Multiple fonts" });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Reload fonts from folder").setDesc(
|
||||||
|
"This button reloades from the folder you specified (it also creates the folder for you)"
|
||||||
|
).addButton((button) => {
|
||||||
|
button.setButtonText("Reload");
|
||||||
|
button.onClick((callback) => {
|
||||||
|
this.plugin.saveSettings();
|
||||||
|
this.plugin.load_plugin();
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.containerEl.createDiv();
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Font").setDesc(
|
||||||
|
`Choose font (If you can't see your fonts, make sure your fonts are in the folder you specified and hit reload.
|
||||||
|
Also if you choose multiple fonts option, we will load and process all fonts in the folder for you. In that Case, enable Custom CSS Mode)`
|
||||||
|
).addDropdown((dropdown) => {
|
||||||
|
for (const opt of options) {
|
||||||
|
dropdown.addOption(opt.name, opt.value);
|
||||||
|
}
|
||||||
|
dropdown.setValue(this.plugin.settings.font).onChange(async (value) => {
|
||||||
|
this.plugin.settings.font = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
await this.plugin.load_plugin();
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (this.plugin.settings.font.toLowerCase() != "none") {
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Force Style").setDesc(
|
||||||
|
"This option should only be used if you have installed a community theme and normal mode doesn't work"
|
||||||
|
).addToggle((toggle) => {
|
||||||
|
toggle.setValue(this.plugin.settings.force_mode);
|
||||||
|
toggle.onChange(async (value) => {
|
||||||
|
this.plugin.settings.force_mode = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
await this.plugin.load_plugin();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Custom CSS Mode").setDesc(
|
||||||
|
"If you want to apply a custom css style rather than default style, choose this."
|
||||||
|
).addToggle((toggle) => {
|
||||||
|
toggle.setValue(this.plugin.settings.custom_css_mode);
|
||||||
|
toggle.onChange(async (value) => {
|
||||||
|
if (this.plugin.settings.custom_css_mode == false) {
|
||||||
|
this.plugin.settings.custom_css = "";
|
||||||
|
}
|
||||||
|
this.plugin.settings.custom_css_mode = value;
|
||||||
|
this.plugin.saveSettings();
|
||||||
|
this.plugin.load_plugin();
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (this.plugin.settings.custom_css_mode) {
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Custom CSS Style").setDesc("Input your custom css style. Use the font filename without extension (in lowercase) as the font-family name. For example, if your font file is 'MyFont.ttf', use 'myfont' in your CSS.").addTextArea(async (text) => {
|
||||||
|
text.onChange(async (new_value) => {
|
||||||
|
this.plugin.settings.custom_css = new_value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
await this.plugin.load_plugin();
|
||||||
|
});
|
||||||
|
text.setDisabled(!this.plugin.settings.custom_css_mode);
|
||||||
|
if (this.plugin.settings.custom_css == "") {
|
||||||
|
let font_family_name = "";
|
||||||
|
try {
|
||||||
|
font_family_name = this.plugin.settings.font.split(".")[0].toLowerCase();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
if (font_family_name == "all") {
|
||||||
|
if (await this.app.vault.adapter.exists(
|
||||||
|
font_folder_path
|
||||||
|
)) {
|
||||||
|
const files = await this.app.vault.adapter.list(
|
||||||
|
font_folder_path
|
||||||
|
);
|
||||||
|
let final_str = "";
|
||||||
|
for (const file of files.files) {
|
||||||
|
const file_name = file.split("/")[2];
|
||||||
|
const font_family = file_name.split(".")[0].toLowerCase();
|
||||||
|
final_str += "\n" + get_custom_css(
|
||||||
|
font_family,
|
||||||
|
"." + font_family
|
||||||
|
);
|
||||||
|
}
|
||||||
|
text.setValue(final_str);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const template = `/* Example CSS for your font: ${font_family_name} */
|
||||||
|
|
||||||
|
/* Apply to all text */
|
||||||
|
:root * {
|
||||||
|
--font-default: '${font_family_name}';
|
||||||
|
--default-font: '${font_family_name}';
|
||||||
|
--font-family-editor: '${font_family_name}';
|
||||||
|
--font-interface-override: '${font_family_name}';
|
||||||
|
--font-text-override: '${font_family_name}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Example: Apply to custom CSS class */
|
||||||
|
.custom-font * {
|
||||||
|
font-family: '${font_family_name}' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Example: Apply to specific elements only */
|
||||||
|
.custom-font h1, .custom-font h2, .custom-font h3 {
|
||||||
|
font-family: '${font_family_name}' !important;
|
||||||
|
}`;
|
||||||
|
text.setValue(template);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text.setValue(this.plugin.settings.custom_css);
|
||||||
|
}
|
||||||
|
text.onChanged();
|
||||||
|
text.inputEl.style.width = "100%";
|
||||||
|
text.inputEl.style.height = "100px";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* nosourcemap */
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "custom-font-loader",
|
||||||
|
"name": "Custom Font Loader",
|
||||||
|
"version": "1.8.0",
|
||||||
|
"minAppVersion": "0.15.0",
|
||||||
|
"description": "Customize your Obsidian vault with any font you want (+ Support for Android and IOS)",
|
||||||
|
"author": "Amir Pourmand",
|
||||||
|
"authorUrl": "https://amirpourmand.ir",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
+4140
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "kanban-bases-view",
|
||||||
|
"name": "Kanban Bases View",
|
||||||
|
"version": "0.10.1",
|
||||||
|
"minAppVersion": "1.10.2",
|
||||||
|
"description": "A kanban-style drag-and-drop custom view for Bases.",
|
||||||
|
"author": "I. Welch Canavan",
|
||||||
|
"authorUrl": "https://welchcanavan.com",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
+617
@@ -0,0 +1,617 @@
|
|||||||
|
/* Kanban View Container */
|
||||||
|
.obk-view-container {
|
||||||
|
container-type: inline-size;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Property Selector */
|
||||||
|
.obk-property-selector {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-property-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-property-select {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--background-primary);
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-property-select:hover {
|
||||||
|
border-color: var(--interactive-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-property-select:focus {
|
||||||
|
outline: 2px solid var(--interactive-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.obk-empty-state {
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban Board */
|
||||||
|
.obk-board {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
flex: 1;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-board::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-board::-webkit-scrollbar-track {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-board::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--background-modifier-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-board::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--background-modifier-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Swimlane mode: stack lanes vertically. The lane body becomes the
|
||||||
|
horizontal column flex (replacing what .obk-board does in flat mode). */
|
||||||
|
.obk-board--with-swimlanes {
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--background-secondary-alt);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-header {
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: var(--background-primary-alt);
|
||||||
|
border-bottom: 1px solid var(--background-modifier-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-normal);
|
||||||
|
text-transform: capitalize;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--background-modifier-border);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
padding: 12px;
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In swimlane mode, each lane grows tall enough to fit the fullest column,
|
||||||
|
and shorter column bodies stretch to that height so their Sortable drop
|
||||||
|
target spans the whole lane row. */
|
||||||
|
.obk-board--with-swimlanes .obk-column {
|
||||||
|
min-height: 0;
|
||||||
|
max-height: none;
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-board--with-swimlanes .obk-column-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: visible;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The outer container caps height in flat mode; release it in swimlane mode
|
||||||
|
so the board grows to fit all lanes and the parent scroll-area scrolls. */
|
||||||
|
.obk-view-container--with-swimlanes {
|
||||||
|
overflow: visible;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsed lane: cap the column body at about 30% less than the original
|
||||||
|
420px height and scroll within the
|
||||||
|
column. The lane and column themselves stay flexible — only the card
|
||||||
|
container is capped. */
|
||||||
|
.obk-swimlane--collapsed .obk-column-body {
|
||||||
|
max-height: 294px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--background-modifier-border);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
--icon-size: var(--icon-xs);
|
||||||
|
transition:
|
||||||
|
background 0.1s ease,
|
||||||
|
color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-toggle:hover {
|
||||||
|
color: var(--text-normal);
|
||||||
|
background: var(--background-modifier-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-toggle:focus-visible {
|
||||||
|
outline: 2px solid var(--background-modifier-border-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
padding: 2px 4px;
|
||||||
|
opacity: 0.5;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-drag-handle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-ghost {
|
||||||
|
opacity: 0.3;
|
||||||
|
background: var(--background-modifier-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-swimlane-body::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
.obk-swimlane-body::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.obk-swimlane-body::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--background-modifier-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.obk-swimlane-body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--background-modifier-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban Column */
|
||||||
|
.obk-column {
|
||||||
|
--obk-column-accent-color: transparent;
|
||||||
|
flex: 0 0 clamp(200px, 60cqw, 280px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: color-mix(in srgb, var(--obk-column-accent-color, transparent) 15%, var(--background-primary-alt));
|
||||||
|
border-bottom: 1px solid var(--background-modifier-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Column color picker button */
|
||||||
|
.obk-column-color-btn {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--background-modifier-border);
|
||||||
|
background: var(--obk-column-accent-color, transparent);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition:
|
||||||
|
transform 0.1s ease,
|
||||||
|
border-color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-color-btn:hover {
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color picker popover */
|
||||||
|
.obk-column-color-popover {
|
||||||
|
position: fixed;
|
||||||
|
background: var(--background-primary);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 164px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-color-swatch {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition:
|
||||||
|
transform 0.1s ease,
|
||||||
|
border-color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-color-swatch:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-color-swatch--active {
|
||||||
|
border-color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-color-none {
|
||||||
|
background: var(--background-modifier-border);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-color-none::before,
|
||||||
|
.obk-column-color-none::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 1.5px;
|
||||||
|
background: var(--text-muted);
|
||||||
|
border-radius: 1px;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-color-none::before {
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-color-none::after {
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
padding: 4px;
|
||||||
|
opacity: 0.5;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-drag-handle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-title {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-normal);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: color-mix(in srgb, var(--obk-column-accent-color, transparent) 15%, var(--background-modifier-border));
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-add-btn,
|
||||||
|
.obk-column-remove-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition:
|
||||||
|
background 0.1s ease,
|
||||||
|
color 0.1s ease,
|
||||||
|
opacity 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-add-btn {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column:hover .obk-column-add-btn,
|
||||||
|
.obk-column-add-btn:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-add-btn:hover,
|
||||||
|
.obk-column-remove-btn:hover {
|
||||||
|
color: var(--text-normal);
|
||||||
|
background: var(--background-modifier-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-add-btn .svg-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-remove-btn {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-body::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-body::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-body::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--background-modifier-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--background-modifier-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban Card */
|
||||||
|
.obk-card {
|
||||||
|
background: var(--background-primary);
|
||||||
|
border: 1px solid
|
||||||
|
color-mix(in srgb, var(--obk-column-accent-color, transparent) 15%, var(--background-modifier-border));
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* targets touch-first devices (tablets, phones) to make the kanban genuinely usable on
|
||||||
|
touch screens — any-pointer: coarse alone would also match hybrid devices (e.g.
|
||||||
|
touchscreen laptops) where the primary pointer is still a mouse */
|
||||||
|
@media (any-pointer: coarse) and (hover: none) {
|
||||||
|
.obk-card {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card--hover {
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card--active {
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--interactive-accent) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-cover {
|
||||||
|
display: block;
|
||||||
|
/* Bleed the cover to the card's inner border edge. Card has padding: 12px,
|
||||||
|
so we expand the width by 24px and pull the box out with negative margins.
|
||||||
|
width: 100% alone only fills the content box and leaves a 12px gap on each side. */
|
||||||
|
width: calc(100% + 24px);
|
||||||
|
margin: -12px -12px 8px -12px;
|
||||||
|
/* aspect-ratio is set inline from the imageAspectRatio config */
|
||||||
|
overflow: hidden;
|
||||||
|
border-top-left-radius: inherit;
|
||||||
|
border-top-right-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-cover--fit-cover img {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-cover--fit-contain img {
|
||||||
|
object-fit: contain;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-title {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-normal);
|
||||||
|
line-height: 1.4;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-preview {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 6px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-property {
|
||||||
|
font-size: var(--font-ui-smaller);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-property-wrap {
|
||||||
|
white-space: normal;
|
||||||
|
text-overflow: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-property-wrap .obk-card-property-value {
|
||||||
|
white-space: normal;
|
||||||
|
text-overflow: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-property-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-property-value {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-property-value p {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-quick-add-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-quick-add-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-quick-add-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drag and Drop States */
|
||||||
|
.obk-card-dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: rotate(2deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-ghost {
|
||||||
|
opacity: 0.3;
|
||||||
|
background: var(--interactive-accent);
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-card-chosen {
|
||||||
|
cursor: grabbing;
|
||||||
|
transform: rotate(2deg);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Column Drag and Drop States */
|
||||||
|
.obk-column-dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obk-column-ghost {
|
||||||
|
opacity: 0.3;
|
||||||
|
background: var(--background-modifier-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sortable placeholder */
|
||||||
|
.obk-sortable-ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
background: var(--interactive-accent);
|
||||||
|
border: 2px dashed var(--interactive-accent);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user