Converting Tests back to C# but still with Playwright

This commit is contained in:
2026-06-03 14:45:18 -04:00
parent 85834466f1
commit 46150d3a69
209 changed files with 1503 additions and 683 deletions
+23
View File
@@ -0,0 +1,23 @@
using NUnit.Framework;
using Tests.Helpers;
namespace Tests;
[SetUpFixture]
public class GlobalSetup
{
[OneTimeSetUp]
public async Task GlobalStart()
{
if (Environment.GetEnvironmentVariable("RUN_AGAINST_PRODUCTION") != "true")
{
await LocalServer.StartAsync();
}
}
[OneTimeTearDown]
public void GlobalStop()
{
LocalServer.Stop();
}
}
+106
View File
@@ -0,0 +1,106 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Tests.Helpers;
public static class LocalServer
{
private static Process? _process;
public static string? BaseUrl { get; private set; }
public static async Task StartAsync()
{
if (_process != null) return;
var root = FindProjectRoot();
var webProject = Path.Combine(root, "Web");
Console.WriteLine($"[DEBUG_LOG] Starting local server for project: {webProject}");
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"run --project \"{webProject}\" --urls http://127.0.0.1:0",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = root
}
};
var tcs = new TaskCompletionSource<string>();
_process.OutputDataReceived += (s, e) =>
{
if (string.IsNullOrEmpty(e.Data)) return;
Console.WriteLine($"[SERVER] {e.Data}");
var match = Regex.Match(e.Data, @"Now listening on:\s+(http://127.0.0.1:\d+)");
if (match.Success)
{
tcs.TrySetResult(match.Groups[1].Value);
}
};
_process.ErrorDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
Console.Error.WriteLine($"[SERVER ERROR] {e.Data}");
};
_process.Start();
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
// Wait for the URL to be parsed from output
BaseUrl = await Task.WhenAny(tcs.Task, Task.Delay(30000)) == tcs.Task
? tcs.Task.Result
: null;
if (BaseUrl == null)
{
Stop();
throw new Exception("Timeout waiting for local server to start and provide a URL.");
}
Console.WriteLine($"[DEBUG_LOG] Local server started at: {BaseUrl}");
}
public static void Stop()
{
if (_process != null && !_process.HasExited)
{
Console.WriteLine("[DEBUG_LOG] Stopping local server...");
_process.Kill(true);
_process.Dispose();
_process = null;
}
}
private static string FindProjectRoot()
{
var current = AppDomain.CurrentDomain.BaseDirectory;
while (!string.IsNullOrEmpty(current) && !File.Exists(Path.Combine(current, "IGP.sln")))
{
var parent = Path.GetDirectoryName(current);
if (parent == current) break;
current = parent;
}
if (string.IsNullOrEmpty(current) || !File.Exists(Path.Combine(current, "IGP.sln")))
{
// Fallback to searching up from current directory if BaseDirectory fails
current = Directory.GetCurrentDirectory();
while (!string.IsNullOrEmpty(current) && !File.Exists(Path.Combine(current, "IGP.sln")))
{
var parent = Path.GetDirectoryName(current);
if (parent == current) break;
current = parent;
}
}
return current ?? throw new Exception("Could not find project root containing IGP.sln");
}
}
+50
View File
@@ -0,0 +1,50 @@
using Microsoft.Playwright;
using Tests.Pages;
using Tests.Shared;
namespace Tests.Helpers;
public class Website
{
public IPage Page { get; }
public bool RunAgainstProduction { get; }
public string BaseUrl { get; }
public Website(IPage page)
{
Page = page;
RunAgainstProduction = Environment.GetEnvironmentVariable("RUN_AGAINST_PRODUCTION") == "true";
BaseUrl = RunAgainstProduction ? "https://igpfanreference.ca" : (LocalServer.BaseUrl ?? "http://localhost:5234");
NavigationBar = new NavigationBar(this);
SearchDialog = new SearchDialog(this);
BuildCalculatorPage = new BuildCalculatorPage(this);
HarassCalculatorPage = new HarassCalculatorPage(this);
DatabasePage = new DatabasePage(this);
DatabaseSinglePage = new DatabaseSinglePage(this);
}
public ILocator Locator(string selector) => Page.Locator(selector);
public ILocator FindById(string id) => Page.Locator($"#{id}");
public NavigationBar NavigationBar { get; }
public SearchDialog SearchDialog { get; }
public BuildCalculatorPage BuildCalculatorPage { get; }
public HarassCalculatorPage HarassCalculatorPage { get; }
public DatabasePage DatabasePage { get; }
public DatabaseSinglePage DatabaseSinglePage { get; }
public async Task GotoAsync(string? path = null)
{
var url = path is null ? BaseUrl : $"{BaseUrl}/{path}";
await Page.GotoAsync(url);
}
public async Task ClickElementAsync(ILocator locator) => await locator.ClickAsync();
public async Task EnterInputAsync(ILocator locator, string value)
{
await locator.FillAsync(value);
await locator.PressAsync("Enter");
}
}
+28
View File
@@ -0,0 +1,28 @@
using Tests.Helpers;
namespace Tests.Pages;
public abstract class BasePage
{
protected Website Website { get; }
protected BasePage(Website website)
{
Website = website;
}
public abstract string Url { get; }
public virtual async Task GotoAsync()
{
await Website.GotoAsync(Url);
}
public async Task<List<string>> GetLinksAsync()
{
var content = Website.FindById("content");
var links = content.Locator("a");
var hrefs = await links.EvaluateAllAsync<string[]>("els => els.map(el => el.getAttribute('href')).filter(Boolean)");
return hrefs.ToList();
}
}
@@ -0,0 +1,48 @@
namespace Tests.Pages.BuildCalculator;
public class ArmyComponent
{
private readonly Website _website;
public ArmyComponent(Website website) => _website = website;
public ILocator ArmyView => _website.Locator(".armyView");
public ILocator DisplayValue(string label) =>
_website.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent");
public ILocator ArmyCards => ArmyView.Locator(".armyCard");
public async Task<string> GetArmyCompletedAtAsync() =>
(await DisplayValue("Army Completed At").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetArmyAttackingAtAsync() =>
(await DisplayValue("Army Attacking At").TextContentAsync())?.Trim() ?? "";
public async Task<IReadOnlyList<string>> GetArmyUnitNamesAsync()
{
var cards = await ArmyCards.AllAsync();
var names = new List<string>();
foreach (var card in cards)
{
var text = (await card.InnerTextAsync()).Trim();
var match = System.Text.RegularExpressions.Regex.Match(text, @"\d+x\s*(.+)");
names.Add(match.Success ? match.Groups[1].Value.Trim() : text);
}
return names;
}
public async Task<IReadOnlyList<(string Name, int Count)>> GetArmyUnitCountsAsync()
{
var cards = await ArmyCards.AllAsync();
var counts = new List<(string, int)>();
foreach (var card in cards)
{
var countEl = card.Locator(".armyCount");
var nameEl = card.Locator("div").Last;
var count = (await countEl.TextContentAsync())?.Replace("x", "").Trim() ?? "0";
var name = (await nameEl.TextContentAsync())?.Trim() ?? "";
counts.Add((name, int.Parse(count)));
}
return counts;
}
}
@@ -0,0 +1,27 @@
namespace Tests.Pages.BuildCalculator;
public class BankComponent
{
private readonly Website _website;
public BankComponent(Website website) => _website = website;
public ILocator BankContainer => _website.Locator(".bankContainer");
public ILocator DisplayValue(string label) =>
BankContainer.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent");
public async Task<string> GetTimeAsync() => (await DisplayValue("Time").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetAlloyAsync() => (await DisplayValue("Alloy").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetEtherAsync() => (await DisplayValue("Ether").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetPyreAsync() => (await DisplayValue("Pyre").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetSupplyAsync() => (await DisplayValue("Supply").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetWorkerCountAsync() =>
(await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(0).TextContentAsync())?.Trim() ?? "";
public async Task<string> GetBusyWorkerCountAsync() =>
(await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(1).TextContentAsync())?.Trim() ?? "";
public async Task<string> GetCreatingWorkerCountAsync() =>
(await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(2).TextContentAsync())?.Trim() ?? "";
}
@@ -0,0 +1,20 @@
namespace Tests.Pages.BuildCalculator;
public class BuildChartComponent
{
private readonly Website _website;
public BuildChartComponent(Website website) => _website = website;
public ILocator ChartsContainer => _website.Locator(".chartsContainer");
public ILocator DisplayValue(string label) =>
_website.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent");
public async Task<string> GetHighestAlloyAsync() => (await DisplayValue("Highest Alloy").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetHighestEtherAsync() => (await DisplayValue("Highest Ether").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetHighestPyreAsync() => (await DisplayValue("Highest Pyre").TextContentAsync())?.Trim() ?? "";
public async Task<string> GetHighestArmyAsync() => (await DisplayValue("Highest Army").TextContentAsync())?.Trim() ?? "";
public async Task<int> GetChartCountAsync() =>
await ChartsContainer.Locator("> div").CountAsync();
}
@@ -0,0 +1,12 @@
namespace Tests.Pages.BuildCalculator;
public class BuildOrderComponent
{
private readonly Website _website;
public BuildOrderComponent(Website website) => _website = website;
public ILocator JsonTextarea => _website.Locator("textarea");
public async Task<string> GetJsonDataAsync() =>
await JsonTextarea.InputValueAsync();
}
@@ -0,0 +1,31 @@
namespace Tests.Pages.BuildCalculator;
public class EntityClickViewComponent
{
private readonly Website _website;
public EntityClickViewComponent(Website website) => _website = website;
public ILocator EntityClickView => _website.Locator(".entityClickView");
public async Task<string?> GetEntityNameAsync()
{
var el = EntityClickView.Locator("#entityName");
if (await el.CountAsync() == 0) return null;
return (await el.TextContentAsync())?.Trim();
}
public async Task<string?> GetEntityHealthAsync()
{
var healthText = EntityClickView.Locator("div").Filter(new() { HasTextRegex = new System.Text.RegularExpressions.Regex("Health", System.Text.RegularExpressions.RegexOptions.IgnoreCase) }).First;
if (await healthText.CountAsync() == 0) return null;
var text = (await healthText.TextContentAsync()) ?? "";
var match = System.Text.RegularExpressions.Regex.Match(text, @"(\d+)");
return match.Success ? match.Groups[1].Value : null;
}
public async Task ClickDetailedViewAsync() =>
await EntityClickView.Locator("button").Filter(new() { HasText = "Detailed" }).ClickAsync();
public async Task ClickPlainViewAsync() =>
await EntityClickView.Locator("button").Filter(new() { HasText = "Plain" }).ClickAsync();
}
@@ -0,0 +1,28 @@
namespace Tests.Pages.BuildCalculator;
public class FilterComponent
{
private readonly Website _website;
public FilterComponent(Website website) => _website = website;
private ILocator FactionSelect =>
_website.Locator("select").Filter(new() { Has = _website.Locator("option:has-text('Aru'), option:has-text(\"Q'Rath\")") });
private ILocator ImmortalSelect =>
_website.Locator("select").Filter(new() { Has = _website.Locator("option:has-text('Orzum'), option:has-text('Ajari'), option:has-text('Atzlan'), option:has-text('Mala'), option:has-text('Xol')") });
public async Task SelectFactionAsync(string faction) =>
await FactionSelect.SelectOptionAsync(faction);
public async Task SelectImmortalAsync(string immortal) =>
await ImmortalSelect.SelectOptionAsync(immortal);
public async Task<string> GetSelectedFactionAsync() =>
await FactionSelect.InputValueAsync();
public async Task<string> GetSelectedImmortalAsync() =>
await ImmortalSelect.InputValueAsync();
public async Task<IReadOnlyList<string>> GetAvailableImmortalsAsync() =>
await ImmortalSelect.Locator("option").AllTextContentsAsync();
}
@@ -0,0 +1,29 @@
namespace Tests.Pages.BuildCalculator;
public class HighlightsComponent
{
private readonly Website _website;
public HighlightsComponent(Website website) => _website = website;
public ILocator HighlightsContainer => _website.Locator(".highlightsContainer");
public ILocator RequestedColumn => HighlightsContainer.Locator("div").Filter(new() { HasText = "Requested" }).Locator("+ div");
public ILocator FinishedColumn => HighlightsContainer.Locator("div").Filter(new() { HasText = "Finished" }).Locator("+ div");
public async Task<IReadOnlyList<string>> GetRequestedItemsAsync() =>
await GetHighlightItemsAsync();
public async Task<IReadOnlyList<string>> GetFinishedItemsAsync() =>
await GetHighlightItemsAsync();
private async Task<IReadOnlyList<string>> GetHighlightItemsAsync()
{
var items = await _website.Locator(".highlightsContainer").Locator("div").Filter(new() { HasTextRegex = new System.Text.RegularExpressions.Regex(@"^\d+\s*\|") }).AllAsync();
var result = new List<string>();
foreach (var item in items)
{
var text = (await item.TextContentAsync())?.Trim();
if (text is not null) result.Add(text);
}
return result;
}
}
@@ -0,0 +1,54 @@
namespace Tests.Pages.BuildCalculator;
public class HotkeyViewerComponent
{
private readonly Website _website;
public HotkeyViewerComponent(Website website) => _website = website;
public ILocator KeyContainer => _website.Locator(".keyContainer");
public async Task<ILocator?> FindKeyButtonAsync(string keyLabel)
{
var upper = keyLabel.ToUpperInvariant();
var buttons = KeyContainer.Locator("> div > div");
var count = await buttons.CountAsync();
for (int i = 0; i < count; i++)
{
var btn = buttons.Nth(i);
var text = (await btn.TextContentAsync())?.Trim().ToUpperInvariant() ?? "";
if (text.StartsWith(upper)) return btn;
}
return null;
}
public async Task ClickKeyAsync(string keyText)
{
var btn = await FindKeyButtonAsync(keyText);
if (btn is null) throw new InvalidOperationException($"Key \"{keyText}\" not found");
await btn.ClickAsync(new() { Force = true });
}
public async Task<string?> GetFirstEntityNameAsync(string keyText)
{
var btn = await FindKeyButtonAsync(keyText);
if (btn is null) return null;
var entities = btn.Locator("> div");
if (await entities.CountAsync() == 0) return null;
return (await entities.First.TextContentAsync())?.Trim();
}
public async Task<IReadOnlyList<string>> GetEntityNamesOnKeyAsync(string keyText)
{
var btn = await FindKeyButtonAsync(keyText);
if (btn is null) return Array.Empty<string>();
var entities = btn.Locator("> div");
var count = await entities.CountAsync();
var names = new List<string>();
for (int i = 0; i < count; i++)
{
var text = (await entities.Nth(i).TextContentAsync())?.Trim();
if (!string.IsNullOrEmpty(text)) names.Add(text);
}
return names;
}
}
@@ -0,0 +1,38 @@
namespace Tests.Pages.BuildCalculator;
public class OptionsComponent
{
private readonly Website _website;
public OptionsComponent(Website website) => _website = website;
private ILocator FormNumberInput(string label) =>
_website.Locator(".formNumberContainer").Filter(new() { HasText = label }).Locator("input[type='number']");
private ILocator ButtonWithLabel(string label) =>
_website.Locator("button").Filter(new() { HasText = label });
public ILocator BuildingInputDelayInput => FormNumberInput("Building Input Delay");
public ILocator WaitTimeInput => FormNumberInput("Wait Time");
public ILocator WaitToInput => FormNumberInput("Wait To");
public ILocator AddWaitButton => ButtonWithLabel("Add Wait").First;
public ILocator AddWaitToButton => ButtonWithLabel("Add Wait").Last;
public async Task SetBuildingInputDelayAsync(int value)
{
await BuildingInputDelayInput.FillAsync(value.ToString());
await BuildingInputDelayInput.PressAsync("Enter");
}
public async Task SetWaitTimeAsync(int value) =>
await WaitTimeInput.FillAsync(value.ToString());
public async Task SetWaitToAsync(int value) =>
await WaitToInput.FillAsync(value.ToString());
public async Task ClickAddWaitAsync() => await AddWaitButton.ClickAsync();
public async Task ClickAddWaitToAsync() => await AddWaitToButton.ClickAsync();
public async Task<string> GetBuildingInputDelayAsync() => await BuildingInputDelayInput.InputValueAsync();
public async Task<string> GetWaitTimeAsync() => await WaitTimeInput.InputValueAsync();
public async Task<string> GetWaitToAsync() => await WaitToInput.InputValueAsync();
}
@@ -0,0 +1,16 @@
namespace Tests.Pages.BuildCalculator;
public class TimelineComponent
{
private readonly Website _website;
public TimelineComponent(Website website) => _website = website;
public ILocator Container =>
_website.Locator(".calculatorGrid > div").Filter(new() { HasText = "Timeline highlights" });
public async Task<bool> ContainsEntityAsync(string name)
{
var text = (await Container.TextContentAsync()) ?? "";
return text.Contains(name);
}
}
@@ -0,0 +1,28 @@
namespace Tests.Pages.BuildCalculator;
public class TimingComponent
{
private readonly Website _website;
public TimingComponent(Website website) => _website = website;
private ILocator FormNumberInput(string label) =>
_website.Locator(".formNumberContainer").Filter(new() { HasText = label }).Locator("input[type='number']");
public ILocator AttackTimeInput => FormNumberInput("Attack Time");
public ILocator TravelTimeInput => FormNumberInput("Travel Time");
public async Task SetAttackTimeAsync(int value)
{
await AttackTimeInput.FillAsync(value.ToString());
await AttackTimeInput.PressAsync("Enter");
}
public async Task SetTravelTimeAsync(int value)
{
await TravelTimeInput.FillAsync(value.ToString());
await TravelTimeInput.PressAsync("Enter");
}
public async Task<string> GetAttackTimeAsync() => await AttackTimeInput.InputValueAsync();
public async Task<string> GetTravelTimeAsync() => await TravelTimeInput.InputValueAsync();
}
+43
View File
@@ -0,0 +1,43 @@
using Tests.Pages.BuildCalculator;
using Tests.Shared;
namespace Tests.Pages;
public class BuildCalculatorPage : BasePage
{
public BuildCalculatorPage(Website website) : base(website)
{
Timing = new TimingComponent(website);
Filter = new FilterComponent(website);
Options = new OptionsComponent(website);
Bank = new BankComponent(website);
Army = new ArmyComponent(website);
Highlights = new HighlightsComponent(website);
BuildOrder = new BuildOrderComponent(website);
Timeline = new TimelineComponent(website);
Hotkeys = new HotkeyViewerComponent(website);
EntityView = new EntityClickViewComponent(website);
Chart = new BuildChartComponent(website);
Toast = new ToastComponent(website);
}
public override string Url => "build-calculator";
public TimingComponent Timing { get; }
public FilterComponent Filter { get; }
public OptionsComponent Options { get; }
public BankComponent Bank { get; }
public ArmyComponent Army { get; }
public HighlightsComponent Highlights { get; }
public BuildOrderComponent BuildOrder { get; }
public TimelineComponent Timeline { get; }
public HotkeyViewerComponent Hotkeys { get; }
public EntityClickViewComponent EntityView { get; }
public BuildChartComponent Chart { get; }
public ToastComponent Toast { get; }
public ILocator CalculatorGrid => Website.Locator(".calculatorGrid");
public ILocator ClearBuildOrderButton => Website.Locator("button").Filter(new() { HasText = "Clear Build Order" });
public async Task ClickClearBuildOrderAsync() => await ClearBuildOrderButton.ClickAsync();
}
+26
View File
@@ -0,0 +1,26 @@
namespace Tests.Pages;
public class DatabasePage : BasePage
{
public DatabasePage(Website website) : base(website) { }
public override string Url => "database";
public async Task FilterNameAsync(string name)
{
var input = Website.FindById("filterName").First;
await Website.EnterInputAsync(input, name);
}
public async Task<string> GetEntityNameAsync(string entityType, string entityName)
{
var el = Website.Locator($"#{entityType.ToLower()}-{entityName.ToLower()}").Locator("#entityName");
return (await el.InnerTextAsync()).Trim();
}
public async Task<string> GetEntityNameByIndexAsync(int index)
{
var el = Website.FindById("entityName").Nth(index);
return (await el.InnerTextAsync()).Trim();
}
}
+25
View File
@@ -0,0 +1,25 @@
namespace Tests.Pages;
public class DatabaseSinglePage : BasePage
{
public DatabaseSinglePage(Website website) : base(website) { }
public override string Url => "database";
public async Task GotoWithSearchAsync(string searchText)
{
await Website.GotoAsync($"{Url}/{searchText}");
}
public async Task<string> GetEntityNameAsync() =>
(await Website.FindById("entityName").InnerTextAsync()).Trim();
public async Task<string> GetEntityHealthAsync() =>
(await Website.FindById("entityHealth").InnerTextAsync()).Trim();
public async Task<string> GetInvalidSearchAsync() =>
(await Website.FindById("invalidSearch").InnerTextAsync()).Trim();
public async Task<string> GetValidSearchAsync() =>
(await Website.FindById("validSearch").InnerTextAsync()).Trim();
}
+48
View File
@@ -0,0 +1,48 @@
namespace Tests.Pages;
public class HarassCalculatorPage : BasePage
{
public HarassCalculatorPage(Website website) : base(website) { }
public override string Url => "harass-calculator";
public async Task SetWorkersLostToHarassAsync(int number) =>
await EnterAndPressAsync("numberOfWorkersLostToHarass", number);
public async Task SetNumberOfTownHallsExistingAsync(int number) =>
await EnterAndPressAsync("numberOfTownHallsExisting", number);
public async Task SetTownHallTravelTimeAsync(int index, int seconds) =>
await EnterInputAtIndexAsync("numberOfTownHallTravelTimes", index, seconds);
public async Task<int> GetTotalAlloyHarassmentAsync() => await ReadIntAsync("totalAlloyHarassment");
public async Task<int> GetWorkerReplacementCostAsync() => await ReadIntAsync("workerReplacementCost");
public async Task<int> GetDelayedMiningCostAsync() => await ReadIntAsync("delayedMiningCost");
public async Task<int> GetAverageTravelTimeAsync() => await ReadIntAsync("getAverageTravelTime");
public async Task<int> GetExampleTotalAlloyLossAsync() => await ReadIntAsync("exampleTotalAlloyLoss");
public async Task<int> GetExampleWorkerCostAsync() => await ReadIntAsync("exampleWorkerCost");
public async Task<int> GetExampleMiningTimeCostAsync() => await ReadIntAsync("exampleMiningTimeCost");
public async Task<int> GetExampleTotalAlloyLossAccurateAsync() => await ReadIntAsync("exampleTotalAlloyLossAccurate");
public async Task<int> GetExampleTotalAlloyLossDifferenceAsync() => await ReadIntAsync("exampleTotalAlloyLossDifference");
public async Task<int> GetExampleTotalAlloyLossAccurateDifferenceAsync() => await ReadIntAsync("exampleTotalAlloyLossAccurateDifference");
private async Task EnterAndPressAsync(string id, int value)
{
var locator = Website.FindById(id);
await Website.EnterInputAsync(locator, value.ToString());
}
private async Task EnterInputAtIndexAsync(string parentId, int index, int value)
{
var inputs = Website.FindById(parentId).Locator("input");
var input = inputs.Nth(index);
await input.FillAsync(value.ToString());
await input.PressAsync("Enter");
}
private async Task<int> ReadIntAsync(string id)
{
var text = await Website.FindById(id).TextContentAsync() ?? "";
return int.Parse(text.Trim());
}
}
+20
View File
@@ -0,0 +1,20 @@
namespace Tests.Shared;
public class NavigationBar
{
private readonly Website _website;
public NavigationBar(Website website) => _website = website;
public ILocator SearchButton => _website.Locator("#desktop-searchButton");
public async Task ClickHomeLinkAsync()
{
await _website.Locator("a:has-text(\"IGP Fan Reference\")").ClickAsync();
}
public async Task<SearchDialog> ClickSearchButtonAsync()
{
await SearchButton.ClickAsync();
return _website.SearchDialog;
}
}
+26
View File
@@ -0,0 +1,26 @@
namespace Tests.Shared;
public class SearchDialog
{
private readonly Website _website;
public SearchDialog(Website website) => _website = website;
public ILocator SearchBackground => _website.FindById("searchBackground");
public ILocator SearchInput => _website.FindById("searchInput");
public async Task CloseDialogAsync()
{
await _website.ClickElementAsync(SearchBackground);
}
public async Task<SearchDialog> SearchAsync(string text)
{
await _website.EnterInputAsync(SearchInput, text);
return this;
}
public async Task SelectSearchEntityAsync(string label)
{
await _website.ClickElementAsync(_website.Locator($"button[label=\"{label}\"]"));
}
}
+36
View File
@@ -0,0 +1,36 @@
namespace Tests.Shared;
public class ToastComponent
{
private readonly Website _website;
public ToastComponent(Website website) => _website = website;
public ILocator Container => _website.Locator(".toastsContainer");
public ILocator Toasts => _website.Locator(".toastsContainer .toastContainer");
public async Task<IReadOnlyList<string>> GetToastTitlesAsync()
{
var titles = await _website.Locator(".toastsContainer .toastTitle").AllTextContentsAsync();
return titles.Select(t => t.Trim()).Where(t => !string.IsNullOrEmpty(t)).ToList();
}
public async Task<bool> HasToastContainingAsync(string text)
{
try
{
await _website.Page.WaitForFunctionAsync(
@"(expected) => {
const titles = document.querySelectorAll('.toastsContainer .toastTitle');
return Array.from(titles).some(t => t.textContent.trim().includes(expected));
}",
text,
new() { Timeout = 3000 }
);
return true;
}
catch
{
return false;
}
}
}
+115
View File
@@ -0,0 +1,115 @@
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace Tests.Specs;
[Parallelizable(ParallelScope.Self)]
[FixtureLifeCycle(LifeCycle.SingleInstance)]
public class BuildCalculatorTests : PageTest
{
private Helpers.Website _website = null!;
[SetUp]
public void CreateWebsite() => _website = new Helpers.Website(Page);
[Test]
public async Task AddEntitiesViaKeyboardQWE()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
await calc.Hotkeys.ClickKeyAsync("TAB");
var keyMap = new Dictionary<string, string> { ["Q"] = "q", ["W"] = "w", ["E"] = "e", ["TAB"] = "Tab" };
foreach (var key in new[] { "Q", "W", "E", "TAB" })
{
var entityNames = await calc.Hotkeys.GetEntityNamesOnKeyAsync(key);
if (entityNames.Count == 0) continue;
await Page.Keyboard.PressAsync(keyMap[key]);
var viewName = await calc.EntityView.GetEntityNameAsync();
Assert.That(viewName, Is.Not.Null.And.Not.Empty);
Assert.That(entityNames, Does.Contain(viewName));
}
}
[Test]
public async Task AddEntitiesViaHotkeysClickTABQWE()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
foreach (var key in new[] { "TAB", "Q", "W", "E" })
{
var entityNames = await calc.Hotkeys.GetEntityNamesOnKeyAsync(key);
if (entityNames.Count == 0) continue;
await calc.Hotkeys.ClickKeyAsync(key);
var viewName = await calc.EntityView.GetEntityNameAsync();
Assert.That(viewName, Is.Not.Null.And.Not.Empty);
Assert.That(entityNames, Does.Contain(viewName));
}
}
[Test]
public async Task AddAcropolisViaQVerifyEntityViewAndTimelineThenClear()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
Assert.That(await calc.Timeline.ContainsEntityAsync("Acropolis"), Is.False);
await calc.Hotkeys.ClickKeyAsync("Q");
Assert.That(await calc.EntityView.GetEntityNameAsync(), Is.EqualTo("Acropolis"));
Assert.That(await calc.Timeline.ContainsEntityAsync("Acropolis"), Is.True);
await calc.ClickClearBuildOrderAsync();
await Task.Delay(1000);
Assert.That(await calc.Timeline.ContainsEntityAsync("Acropolis"), Is.False);
Assert.That(await calc.EntityView.GetEntityNameAsync(), Is.Null);
}
[Test]
public async Task MissingRequirementsToastWhenBuildingSoulFoundryWithoutLegionHall()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
await calc.Hotkeys.ClickKeyAsync("E");
var hasToast = await calc.Toast.HasToastContainingAsync("Missing Requirements");
Assert.That(hasToast, Is.True);
}
[Test]
public async Task NotEnoughEtherToastWhenBuildingSoulFoundryAfterLegionHall()
{
var calc = _website.BuildCalculatorPage;
await calc.GotoAsync();
await calc.Filter.SelectFactionAsync("Q'Rath");
await calc.Filter.SelectImmortalAsync("Orzum");
await calc.Hotkeys.ClickKeyAsync("W");
await calc.Hotkeys.ClickKeyAsync("E");
var hasToast = await calc.Toast.HasToastContainingAsync("Not Enough Ether");
Assert.That(hasToast, Is.True);
}
}
+43
View File
@@ -0,0 +1,43 @@
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace Tests.Specs;
[Parallelizable(ParallelScope.Self)]
[FixtureLifeCycle(LifeCycle.SingleInstance)]
public class HarassCalculatorTests : PageTest
{
private Helpers.Website _website = null!;
[SetUp]
public void CreateWebsite() => _website = new Helpers.Website(Page);
[Test]
public async Task CalculatorInput()
{
var page = _website.HarassCalculatorPage;
await page.GotoAsync();
await page.SetWorkersLostToHarassAsync(3);
await page.SetNumberOfTownHallsExistingAsync(2);
await page.SetTownHallTravelTimeAsync(0, 30);
var result = await page.GetTotalAlloyHarassmentAsync();
Assert.That(result, Is.EqualTo(240));
}
[Test]
public async Task CalculatedExampleInformation()
{
var page = _website.HarassCalculatorPage;
await page.GotoAsync();
Assert.Multiple(async () =>
{
Assert.That(await page.GetExampleTotalAlloyLossAsync(), Is.EqualTo(720));
Assert.That(await page.GetExampleWorkerCostAsync(), Is.EqualTo(300));
Assert.That(await page.GetExampleMiningTimeCostAsync(), Is.EqualTo(420));
Assert.That(await page.GetExampleTotalAlloyLossAccurateAsync(), Is.EqualTo(450));
Assert.That(await page.GetExampleTotalAlloyLossDifferenceAsync(), Is.EqualTo(300));
Assert.That(await page.GetExampleTotalAlloyLossAccurateDifferenceAsync(), Is.EqualTo(270));
});
}
}
+41
View File
@@ -0,0 +1,41 @@
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace Tests.Specs;
[Parallelizable(ParallelScope.Self)]
[FixtureLifeCycle(LifeCycle.SingleInstance)]
public class LinksTests : PageTest
{
private Helpers.Website _website = null!;
[SetUp]
public void CreateWebsite() => _website = new Helpers.Website(Page);
[Test]
public async Task VerifyPageLinks()
{
_website = new Helpers.Website(Page);
await _website.HarassCalculatorPage.GotoAsync();
var harassLinks = await _website.HarassCalculatorPage.GetLinksAsync();
foreach (var link in harassLinks) await VerifyLinkAsync(link);
await _website.DatabasePage.GotoAsync();
var dbLinks = await _website.DatabasePage.GetLinksAsync();
foreach (var link in dbLinks) await VerifyLinkAsync(link);
await _website.DatabaseSinglePage.GotoWithSearchAsync("throne");
var singleLinks = await _website.DatabaseSinglePage.GetLinksAsync();
foreach (var link in singleLinks) await VerifyLinkAsync(link);
}
private static async Task VerifyLinkAsync(string link)
{
if (link.StartsWith("mailto")) return;
using var client = new System.Net.Http.HttpClient();
var response = await client.GetAsync(link);
Assert.That(response.IsSuccessStatusCode, Is.True, $"Link '{link}' returned {response.StatusCode}");
}
}
+68
View File
@@ -0,0 +1,68 @@
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace Tests.Specs;
[Parallelizable(ParallelScope.Self)]
[FixtureLifeCycle(LifeCycle.SingleInstance)]
public class SearchFeaturesTests : PageTest
{
private Helpers.Website _website = null!;
[SetUp]
public void CreateWebsite() => _website = new Helpers.Website(Page);
[Test]
public async Task DesktopOpenCloseSearchDialog()
{
await _website.GotoAsync();
await _website.NavigationBar.ClickSearchButtonAsync();
await _website.SearchDialog.CloseDialogAsync();
await _website.NavigationBar.ClickHomeLinkAsync();
}
[Test]
public async Task DesktopSearchForThrone()
{
await _website.GotoAsync();
await _website.NavigationBar.ClickSearchButtonAsync();
await _website.SearchDialog.SearchAsync("Throne");
await _website.SearchDialog.SelectSearchEntityAsync("Throne");
var name = await _website.DatabaseSinglePage.GetEntityNameAsync();
var health = await _website.DatabaseSinglePage.GetEntityHealthAsync();
Assert.That(name, Is.EqualTo("Throne"));
Assert.That(health.Trim(), Is.Not.Empty);
}
[Test]
public async Task DesktopFilterForThrone()
{
var page = _website.DatabasePage;
await page.GotoAsync();
await page.FilterNameAsync("Throne");
var name = await page.GetEntityNameByIndexAsync(0);
Assert.That(name, Is.EqualTo("Throne"));
}
[Test]
public async Task SeeThroneByDefault()
{
var page = _website.DatabasePage;
await page.GotoAsync();
var name = await page.GetEntityNameAsync("army", "throne");
Assert.That(name, Is.EqualTo("Throne"));
}
[Test]
public async Task DirectLinkNotThroneFailure()
{
var page = _website.DatabaseSinglePage;
await page.GotoWithSearchAsync("not throne");
var invalidSearch = await page.GetInvalidSearchAsync();
var validSearch = await page.GetValidSearchAsync();
Assert.That(invalidSearch, Is.EqualTo("not throne"));
Assert.That(validSearch, Is.EqualTo("Throne"));
}
}
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.52.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.Playwright" />
<Using Include="Tests.Helpers" />
</ItemGroup>
</Project>