diff --git a/AOW4.SeleniumTests/AOW4.SeleniumTests.csproj b/AOW4.SeleniumTests/AOW4.SeleniumTests.csproj
new file mode 100644
index 0000000..2ab7cb8
--- /dev/null
+++ b/AOW4.SeleniumTests/AOW4.SeleniumTests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net10.0
+ false
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AOW4.SeleniumTests/Driver/DriverFactory.cs b/AOW4.SeleniumTests/Driver/DriverFactory.cs
new file mode 100644
index 0000000..aa2643a
--- /dev/null
+++ b/AOW4.SeleniumTests/Driver/DriverFactory.cs
@@ -0,0 +1,27 @@
+using System;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Chrome;
+
+namespace AOW4.SeleniumTests.Driver;
+
+public static class DriverFactory
+{
+ public static IWebDriver CreateChromeDriver()
+ {
+ var options = new ChromeOptions();
+ var headless = Environment.GetEnvironmentVariable("HEADLESS");
+ if (!string.IsNullOrEmpty(headless) && (headless == "1" || headless.Equals("true", StringComparison.OrdinalIgnoreCase)))
+ {
+ options.AddArgument("--headless=new");
+ }
+
+ options.AddArgument("--disable-gpu");
+ options.AddArgument("--no-sandbox");
+ options.AddArgument("--disable-dev-shm-usage");
+
+ var service = ChromeDriverService.CreateDefaultService();
+ service.HideCommandPromptWindow = true;
+
+ return new ChromeDriver(service, options);
+ }
+}
diff --git a/AOW4.SeleniumTests/Pages/CounterPage.cs b/AOW4.SeleniumTests/Pages/CounterPage.cs
new file mode 100644
index 0000000..877e249
--- /dev/null
+++ b/AOW4.SeleniumTests/Pages/CounterPage.cs
@@ -0,0 +1,15 @@
+using OpenQA.Selenium;
+
+namespace AOW4.SeleniumTests.Pages;
+
+public class CounterPage
+{
+ private readonly IWebDriver _driver;
+ public CounterPage(IWebDriver driver) => _driver = driver;
+
+ public bool IsAt()
+ {
+ var url = _driver.Url ?? string.Empty;
+ return url.Contains("counter", System.StringComparison.OrdinalIgnoreCase) || _driver.PageSource.Contains("Counter");
+ }
+}
diff --git a/AOW4.SeleniumTests/Pages/HomePage.cs b/AOW4.SeleniumTests/Pages/HomePage.cs
new file mode 100644
index 0000000..8f571d2
--- /dev/null
+++ b/AOW4.SeleniumTests/Pages/HomePage.cs
@@ -0,0 +1,15 @@
+using OpenQA.Selenium;
+
+namespace AOW4.SeleniumTests.Pages;
+
+public class HomePage
+{
+ private readonly IWebDriver _driver;
+ public HomePage(IWebDriver driver) => _driver = driver;
+
+ public bool IsAt()
+ {
+ var url = _driver.Url ?? string.Empty;
+ return url.EndsWith("/") || url.Contains("/index", System.StringComparison.OrdinalIgnoreCase) || _driver.PageSource.Contains("Home");
+ }
+}
diff --git a/AOW4.SeleniumTests/Pages/NavMenuPage.cs b/AOW4.SeleniumTests/Pages/NavMenuPage.cs
new file mode 100644
index 0000000..9b3a599
--- /dev/null
+++ b/AOW4.SeleniumTests/Pages/NavMenuPage.cs
@@ -0,0 +1,27 @@
+using System.Linq;
+using OpenQA.Selenium;
+
+namespace AOW4.SeleniumTests.Pages;
+
+public class NavMenuPage
+{
+ private readonly IWebDriver _driver;
+
+ public NavMenuPage(IWebDriver driver)
+ {
+ _driver = driver;
+ }
+
+ public void ClickLinkByText(string linkText)
+ {
+ var link = _driver.FindElements(By.CssSelector("a[href]"))
+ .FirstOrDefault(e => !string.IsNullOrWhiteSpace(e.Text) && e.Text.Trim().Equals(linkText, System.StringComparison.OrdinalIgnoreCase));
+
+ if (link == null)
+ {
+ throw new NoSuchElementException($"Link with text '{linkText}' not found in the page.");
+ }
+
+ link.Click();
+ }
+}
diff --git a/AOW4.SeleniumTests/Pages/WeatherPage.cs b/AOW4.SeleniumTests/Pages/WeatherPage.cs
new file mode 100644
index 0000000..e482da8
--- /dev/null
+++ b/AOW4.SeleniumTests/Pages/WeatherPage.cs
@@ -0,0 +1,15 @@
+using OpenQA.Selenium;
+
+namespace AOW4.SeleniumTests.Pages;
+
+public class WeatherPage
+{
+ private readonly IWebDriver _driver;
+ public WeatherPage(IWebDriver driver) => _driver = driver;
+
+ public bool IsAt()
+ {
+ var url = _driver.Url ?? string.Empty;
+ return url.Contains("weather", System.StringComparison.OrdinalIgnoreCase) || _driver.PageSource.Contains("Weather");
+ }
+}
diff --git a/AOW4.SeleniumTests/README.md b/AOW4.SeleniumTests/README.md
new file mode 100644
index 0000000..9ac5807
--- /dev/null
+++ b/AOW4.SeleniumTests/README.md
@@ -0,0 +1,20 @@
+# AOW4 Selenium Tests
+
+Requirements:
+- .NET 10 SDK
+- Google Chrome installed (compatible with ChromeDriver package)
+- The AOW4 web app running locally (by default at `http://localhost:5000`) or set `BASE_URL` env var.
+
+Run tests:
+
+```powershell
+# optional: run headless
+$env:HEADLESS = "1"
+# optional: point to running app
+$env:BASE_URL = "http://localhost:5000"
+dotnet test AOW4.SeleniumTests\AOW4.SeleniumTests.csproj
+```
+
+Notes:
+- Navigation tests use the UI nav links — ensure the app is running before executing tests.
+- Broken links scanner sends HTTP HEAD requests and falls back to GET if needed.
diff --git a/AOW4.SeleniumTests/Tests/BaseTest.cs b/AOW4.SeleniumTests/Tests/BaseTest.cs
new file mode 100644
index 0000000..5b9db07
--- /dev/null
+++ b/AOW4.SeleniumTests/Tests/BaseTest.cs
@@ -0,0 +1,36 @@
+using NUnit.Framework;
+using OpenQA.Selenium;
+using AOW4.SeleniumTests.Driver;
+
+namespace AOW4.SeleniumTests.Tests;
+
+[TestFixture]
+public abstract class BaseTest
+{
+ protected IWebDriver Driver = null!;
+ protected string BaseUrl => System.Environment.GetEnvironmentVariable("BASE_URL") ?? "http://localhost:5000";
+
+ [OneTimeSetUp]
+ public void GlobalSetup()
+ {
+ Driver = DriverFactory.CreateChromeDriver();
+ Driver.Manage().Window.Maximize();
+ }
+
+ [OneTimeTearDown]
+ public void GlobalTeardown()
+ {
+ try
+ {
+ Driver.Quit();
+ }
+ catch
+ {
+ }
+ }
+
+ protected void GoHome()
+ {
+ Driver.Navigate().GoToUrl(BaseUrl);
+ }
+}
diff --git a/AOW4.SeleniumTests/Tests/BrokenLinksTest.cs b/AOW4.SeleniumTests/Tests/BrokenLinksTest.cs
new file mode 100644
index 0000000..87d57d3
--- /dev/null
+++ b/AOW4.SeleniumTests/Tests/BrokenLinksTest.cs
@@ -0,0 +1,74 @@
+using NUnit.Framework;
+using System.Net.Http;
+using System.Collections.Generic;
+using System.Linq;
+using OpenQA.Selenium;
+
+namespace AOW4.SeleniumTests.Tests;
+
+[TestFixture]
+public class BrokenLinksTest : BaseTest
+{
+ [Test]
+ public void ScanAndReportBrokenLinks()
+ {
+ GoHome();
+
+ var anchors = Driver.FindElements(By.CssSelector("a[href]"))
+ .Select(a => a.GetAttribute("href") ?? string.Empty)
+ .Where(h => !string.IsNullOrWhiteSpace(h))
+ .Distinct()
+ .ToList();
+
+ var failures = new List();
+ using var client = new HttpClient();
+
+ foreach (var raw in anchors)
+ {
+ if (raw.StartsWith("javascript:", System.StringComparison.OrdinalIgnoreCase))
+ continue;
+ if (raw.StartsWith("mailto:", System.StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ System.Uri uri;
+ try
+ {
+ uri = new System.Uri(raw, System.UriKind.RelativeOrAbsolute);
+ if (!uri.IsAbsoluteUri)
+ {
+ uri = new System.Uri(new System.Uri(BaseUrl), raw);
+ }
+ }
+ catch
+ {
+ failures.Add($"Invalid URI: {raw}");
+ continue;
+ }
+
+ try
+ {
+ using var req = new HttpRequestMessage(HttpMethod.Head, uri);
+ var resp = client.Send(req);
+ if (!resp.IsSuccessStatusCode)
+ {
+ // try GET as fallback
+ using var greq = new HttpRequestMessage(HttpMethod.Get, uri);
+ var gresp = client.Send(greq);
+ if (!gresp.IsSuccessStatusCode)
+ {
+ failures.Add($"{(int)gresp.StatusCode} {uri}");
+ }
+ }
+ }
+ catch (System.Exception ex)
+ {
+ failures.Add($"Error checking {uri}: {ex.Message}");
+ }
+ }
+
+ if (failures.Any())
+ {
+ Assert.Fail("Broken links found:\n" + string.Join("\n", failures));
+ }
+ }
+}
diff --git a/AOW4.SeleniumTests/Tests/NavigationTests.cs b/AOW4.SeleniumTests/Tests/NavigationTests.cs
new file mode 100644
index 0000000..851eed1
--- /dev/null
+++ b/AOW4.SeleniumTests/Tests/NavigationTests.cs
@@ -0,0 +1,25 @@
+using NUnit.Framework;
+using AOW4.SeleniumTests.Pages;
+
+namespace AOW4.SeleniumTests.Tests;
+
+[TestFixture]
+public class NavigationTests : BaseTest
+{
+ [TestCase("Home", "/")]
+ [TestCase("Counter", "/counter")]
+ [TestCase("Weather", "/weather")]
+ public void ClickNavLink_NavigatesToPage(string linkText, string expectedPath)
+ {
+ GoHome();
+
+ var nav = new NavMenuPage(Driver);
+ nav.ClickLinkByText(linkText);
+
+ // small wait for navigation
+ System.Threading.Thread.Sleep(700);
+
+ Assert.IsTrue(Driver.Url.Contains(expectedPath, System.StringComparison.OrdinalIgnoreCase) || Driver.PageSource.Contains(linkText),
+ $"Expected to be on route containing '{expectedPath}' after clicking '{linkText}', but was '{Driver.Url}'");
+ }
+}
diff --git a/AOW4.slnx b/AOW4.slnx
index be47bad..6361106 100644
--- a/AOW4.slnx
+++ b/AOW4.slnx
@@ -1,4 +1,5 @@
+