diff --git a/.gitignore b/.gitignore index 0afa44753..4119589ad 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ obj /WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.jfm /WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Database/LearningHub.Nhs.Migration.Staging.Database.dbmdl /WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Database/LearningHub.Nhs.Migration.Staging.Database.jfm +/LearningHub.Nhs.WebUI.AutomatedUiTests/appsettings.Development.json diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/AccessibilityTests/AccessibilityTestsBase.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/AccessibilityTests/AccessibilityTestsBase.cs new file mode 100644 index 000000000..c8409d921 --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/AccessibilityTests/AccessibilityTestsBase.cs @@ -0,0 +1,59 @@ +namespace LearningHub.Nhs.WebUI.AutomatedUiTests.AccessibilityTests +{ + using FluentAssertions; + using LearningHub.Nhs.WebUI.AutomatedUiTests.TestFixtures; + using LearningHub.Nhs.WebUI.Startup; + using OpenQA.Selenium; + using Selenium.Axe; + using Xunit; + + /// + /// Accessibility Tests Base. + /// + [Collection("Selenium test collection")] + public class AccessibilityTestsBase + { + /// + /// Gets the base URL for the tests. + /// + internal readonly string BaseUrl; + + /// + /// Gets the WebDriver used for the tests. + /// + internal readonly IWebDriver Driver; + + /// + /// Initializes a new instance of the class. + /// + /// fixture. + public AccessibilityTestsBase(AccessibilityTestsFixture fixture) + { + this.BaseUrl = fixture.BaseUrl; + this.Driver = fixture.Driver; + } + + /// + /// Analyze Page Heading And Accessibility. + /// + /// Page Title. + public void AnalyzePageHeadingAndAccessibility(string pageTitle) + { + this.ValidatePageHeading(pageTitle); + + // then + var axeResult = new AxeBuilder(this.Driver).Analyze(); + axeResult.Violations.Should().BeEmpty(); + } + + /// + /// ValidatePageHeading. + /// + /// Page Title. + public void ValidatePageHeading(string pageTitle) + { + var h1Element = this.Driver.FindElement(By.TagName("h1")); + h1Element.Text.Should().BeEquivalentTo(pageTitle); + } + } +} diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs new file mode 100644 index 000000000..eac9ac28a --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs @@ -0,0 +1,35 @@ +namespace LearningHub.Nhs.WebUI.AutomatedUiTests.AccessibilityTests +{ + using LearningHub.Nhs.WebUI.AutomatedUiTests.TestFixtures; + using Xunit; + + /// + /// BasicAccessibilityTests. + /// + public class BasicAccessibilityTests : AccessibilityTestsBase, IClassFixture + { + /// + /// Initializes a new instance of the class. + /// BasicAccessibilityTests. + /// + /// fixture. + public BasicAccessibilityTests(AccessibilityTestsFixture fixture) + : base(fixture) + { + } + + [Theory] + [InlineData("/Home/Index", "A platform for learning and sharing resources")] + + public void PageHasNoAccessibilityErrors(string url, string pageTitle) + { + // when + this.Driver.Navigate().GoToUrl(this.BaseUrl + url); + + // then + this.AnalyzePageHeadingAndAccessibility(pageTitle); + + this.Driver.Dispose(); + } + } +} diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/AccessibilityTests/MyAccountAccessibiltyTests.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/AccessibilityTests/MyAccountAccessibiltyTests.cs new file mode 100644 index 000000000..7990f0fa8 --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/AccessibilityTests/MyAccountAccessibiltyTests.cs @@ -0,0 +1,58 @@ +namespace LearningHub.Nhs.WebUI.AutomatedUiTests.AccessibilityTests +{ + using FluentAssertions; + using LearningHub.Nhs.WebUI.AutomatedUiTests.TestFixtures; + using LearningHub.Nhs.WebUI.AutomatedUiTests.TestHelpers; + using Selenium.Axe; + using Xunit; + + /// + /// MyAccountAccessibiltyTests. + /// + public class MyAccountAccessibiltyTests : AccessibilityTestsBase, + IClassFixture + { + /// + /// Initializes a new instance of the class. + /// MyAccountAccessibiltyTests. + /// + /// fixture. + public MyAccountAccessibiltyTests(AuthenticatedAccessibilityTestsFixture fixture) + : base(fixture) + { + } + + /// + /// MyAccount Page Has Accessibility Errors. + /// + [Fact] + public void MyAccountPageHasAccessibilityErrors() + { + // given + const string myaccountsUrl = "/myaccount"; + + // when + this.Driver.Navigate().GoToUrl(this.BaseUrl + myaccountsUrl); + var result = new AxeBuilder(this.Driver).Analyze(); + + // then + CheckAccessibilityResult(result); + + // Dispose driver + this.Driver.LogOutUser(this.BaseUrl); + } + + private static void CheckAccessibilityResult(AxeResult result) + { + // Expect axe violation + result.Violations.Should().HaveCount(6); + + var violation = result.Violations[1]; + + violation.Id.Should().Be("landmark-contentinfo-is-top-level"); + violation.Nodes.Should().HaveCount(1); + violation.Nodes[0].Target.Should().HaveCount(1); + violation.Nodes[0].Target[0].Selector.Should().Be("footer > footer"); + } + } +} diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/GlobalSuppressions.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/GlobalSuppressions.cs new file mode 100644 index 000000000..01866c145 --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/GlobalSuppressions.cs @@ -0,0 +1,13 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Allowed", Scope = "member", Target = "~F:LearningHub.Nhs.WebUI.AutomatedUiTests.SeleniumServerFactory.RootUri")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Allowed", Scope = "member", Target = "~F:LearningHub.Nhs.WebUI.AutomatedUiTests.TestFixtures.AccessibilityTestsFixture.Driver")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Allowed", Scope = "member", Target = "~F:LearningHub.Nhs.WebUI.AutomatedUiTests.TestFixtures.AccessibilityTestsFixture.BaseUrl")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Allowed", Scope = "member", Target = "~F:LearningHub.Nhs.WebUI.AutomatedUiTests.AccessibilityTests.AccessibilityTestsBase.BaseUrl")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Allowed", Scope = "member", Target = "~F:LearningHub.Nhs.WebUI.AutomatedUiTests.AccessibilityTests.AccessibilityTestsBase.Driver")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Regions allowed", Scope = "member", Target = "~M:LearningHub.Nhs.WebUI.AutomatedUiTests.AccessibilityTests.BasicAccessibilityTests.PageHasNoAccessibilityErrors(System.String,System.String)")] diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj new file mode 100644 index 000000000..719ae88fb --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj @@ -0,0 +1,55 @@ + + + + net6.0 + enable + enable + + false + + True + + + + + + + + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/SeleniumServerFactory.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/SeleniumServerFactory.cs new file mode 100644 index 000000000..19edcdaef --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/SeleniumServerFactory.cs @@ -0,0 +1,41 @@ +namespace LearningHub.Nhs.WebUI.AutomatedUiTests +{ + using System; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using LearningHub.Nhs.WebUI.AutomatedUiTests.TestHelpers; + using LearningHub.Nhs.WebUI.Startup; + using Microsoft.AspNetCore; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Hosting.Server.Features; + using Microsoft.AspNetCore.Mvc.Testing; + using Microsoft.AspNetCore.TestHost; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Configuration.Json; + using Serilog; + + /// + /// SeleniumServerFactory. + /// + public class SeleniumServerFactory + { + /// + /// Root Uri. + /// + public string RootUri; + + /// + /// Initializes a new instance of the class. + /// + public SeleniumServerFactory() + { + IConfiguration configuration = ConfigurationHelper.GetConfiguration(); + this.RootUri = configuration["LearningHubWebUiUrl"]; + } + } +} diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/TestFixtures/AccessibilityTestsFixture.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestFixtures/AccessibilityTestsFixture.cs new file mode 100644 index 000000000..9ad27df2a --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestFixtures/AccessibilityTestsFixture.cs @@ -0,0 +1,42 @@ +namespace LearningHub.Nhs.WebUI.AutomatedUiTests.TestFixtures +{ + using LearningHub.Nhs.WebUI.AutomatedUiTests.TestHelpers; + using OpenQA.Selenium; + + /// + /// Represents a fixture for accessibility tests. + /// + public class AccessibilityTestsFixture + { + /// + /// Gets the base URL for the tests. + /// + internal readonly string BaseUrl; + + /// + /// Gets the WebDriver used for the tests. + /// + internal readonly IWebDriver Driver; + + private readonly SeleniumServerFactory factory; + + /// + /// Initializes a new instance of the class. + /// + public AccessibilityTestsFixture() + { + this.factory = new SeleniumServerFactory(); + this.BaseUrl = this.factory.RootUri; + this.Driver = DriverHelper.CreateHeadlessChromeDriver(); + } + + /// + /// Dispose. + /// + public void Dispose() + { + this.Driver.Quit(); + this.Driver.Dispose(); + } + } +} diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/TestFixtures/AuthenticatedAccessibilityTestsFixture.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestFixtures/AuthenticatedAccessibilityTestsFixture.cs new file mode 100644 index 000000000..130e05036 --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestFixtures/AuthenticatedAccessibilityTestsFixture.cs @@ -0,0 +1,32 @@ +namespace LearningHub.Nhs.WebUI.AutomatedUiTests.TestFixtures +{ + using LearningHub.Nhs.WebUI.AutomatedUiTests.TestHelpers; + using Microsoft.Extensions.Configuration; + + /// + /// AuthenticatedAccessibilityTestsFixture. + /// + /// TStartup. + public class AuthenticatedAccessibilityTestsFixture : AccessibilityTestsFixture + { + /// + /// Initializes a new instance of the class. + /// + public AuthenticatedAccessibilityTestsFixture() + { + IConfiguration configuration = ConfigurationHelper.GetConfiguration(); + string adminUsername = configuration["AdminUser:Username"]; + string adminPassword = configuration["AdminUser:Password"]; + this.Driver.LogUserInAsAdmin(this.BaseUrl, adminUsername, adminPassword); + } + + /// + /// Dispose. + /// + public new void Dispose() + { + this.Driver.LogOutUser(this.BaseUrl); + base.Dispose(); + } + } +} diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/TestHelpers/ConfigurationHelper.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestHelpers/ConfigurationHelper.cs new file mode 100644 index 000000000..660575162 --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestHelpers/ConfigurationHelper.cs @@ -0,0 +1,22 @@ +namespace LearningHub.Nhs.WebUI.AutomatedUiTests.TestHelpers +{ + using Microsoft.Extensions.Configuration; + + /// + /// ConfigurationHelper. + /// + public static class ConfigurationHelper + { + /// + /// GetConfiguration. + /// + /// IConfiguration. + public static IConfiguration GetConfiguration() + { + return new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.Development.json") + .Build(); + } + } +} diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/TestHelpers/DriverHelper.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestHelpers/DriverHelper.cs new file mode 100644 index 000000000..9d467b946 --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestHelpers/DriverHelper.cs @@ -0,0 +1,107 @@ +namespace LearningHub.Nhs.WebUI.AutomatedUiTests.TestHelpers +{ + using OpenQA.Selenium; + using OpenQA.Selenium.Chrome; + using OpenQA.Selenium.Support.UI; + + /// + /// Driver Helper. + /// + public static class DriverHelper + { + /// + /// Create Headless ChromeDriver. + /// + /// Chrome Driver. + public static ChromeDriver CreateHeadlessChromeDriver() + { + var chromeOptions = new ChromeOptions(); + + chromeOptions.AddArgument("--headless"); + return new ChromeDriver(chromeOptions); + } + + /// + /// Fill Text Input. + /// + /// WebDriver. + /// inputId. + /// inputText. + public static void FillTextInput(this IWebDriver driver, string inputId, string inputText) + { + var answer = driver.FindElement(By.Id(inputId)); + answer.Clear(); + answer.SendKeys(inputText); + } + + /// + /// ClickButtonByText. + /// + /// WebDriver. + /// text. + public static void ClickButtonByText(this IWebDriver driver, string text) + { + var addButton = driver.FindElement(By.XPath($"//button[.='{text}']")); + addButton.Click(); + } + + /// + /// ClickLinkContainingText. + /// + /// WebDriver. + /// text. + public static void ClickLinkContainingText(this IWebDriver driver, string text) + { + var foundLink = driver.FindElement(By.XPath($"//a[contains(., '{text}')]")); + foundLink.Click(); + } + + /// + /// SelectDropdownItemValue. + /// + /// WebDriver. + /// dropdownId. + /// selectedValue. + public static void SelectDropdownItemValue(this IWebDriver driver, string dropdownId, string selectedValue) + { + var dropdown = new SelectElement(driver.FindElement(By.Id(dropdownId))); + dropdown.SelectByValue(selectedValue); + } + + /// + /// SetCheckboxState. + /// + /// WebDriver. + /// inputId. + /// checkState. + public static void SetCheckboxState(this IWebDriver driver, string inputId, bool checkState) + { + var answer = driver.FindElement(By.Id(inputId)); + if (answer.Selected != checkState) + { + answer.Click(); + } + } + + /// + /// Submit Form. + /// + /// WebDriver. + public static void SubmitForm(this IWebDriver driver) + { + var selectPromptForm = driver.FindElement(By.TagName("form")); + selectPromptForm.Submit(); + } + + /// + /// Select Radio Option By Id. + /// + /// WebDriver. + /// radio Id. + public static void SelectRadioOptionById(this IWebDriver driver, string radioId) + { + var radioInput = driver.FindElement(By.Id(radioId)); + radioInput.Click(); + } + } +} diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/TestHelpers/LoginHelper.cs b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestHelpers/LoginHelper.cs new file mode 100644 index 000000000..8f9786d27 --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/TestHelpers/LoginHelper.cs @@ -0,0 +1,79 @@ +namespace LearningHub.Nhs.WebUI.AutomatedUiTests.TestHelpers +{ + using OpenQA.Selenium; + using OpenQA.Selenium.Support.UI; + + /// + /// LoginHelper. + /// + public static class LoginHelper + { + /// + /// Get LogUserInAsAdmin. + /// + /// WebDriver. + /// baseUrl. + /// adminName. + /// adminPassword. + public static void LogUserInAsAdmin(this IWebDriver driver, string baseUrl, string adminName, string adminPassword) + { + driver.Navigate().GoToUrl(baseUrl + "/Login"); + var username = driver.FindElement(By.Id("Username")); + username.SendKeys(adminName); + + var password = driver.FindElement(By.Id("Password")); + password.SendKeys(adminPassword); + + var submitButton = driver.FindElement(By.TagName("form")); + submitButton.Submit(); + } + + /// + /// LogOutUser. + /// + /// WebDriver. + /// baseUrl. + public static void LogOutUser(this IWebDriver driver, string baseUrl) + { + driver.Navigate().GoToUrl(baseUrl); + + try + { + // Maximum time to wait for the element in seconds + int maxWaitTimeInSeconds = 10; + + // Find the element using XPath + IWebElement logoutLink = null; + + for (int i = 0; i < maxWaitTimeInSeconds; i++) + { + try + { + logoutLink = driver.FindElement(By.CssSelector("a.nhsuk-account__login--link[href='/Home/Logout']")); + if (logoutLink.Displayed) + { + break; // Exit the loop if element is found and displayed + } + } + catch (NoSuchElementException) + { + // Element not found yet, wait for a second and try again + Thread.Sleep(1000); + } + } + + // Check if the element is found and displayed + if (logoutLink != null && logoutLink.Displayed) + { + // Perform an action on the element (e.g., click) + logoutLink.Click(); + } + } + finally + { + // Close the browser window + driver.Quit(); + } + } + } +} diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/appsettings.json b/LearningHub.Nhs.WebUI.AutomatedUiTests/appsettings.json new file mode 100644 index 000000000..223c0c93f --- /dev/null +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/appsettings.json @@ -0,0 +1,7 @@ +{ + "LearningHubWebUiUrl": "", + "AdminUser": { + "Username": "", + "Password": "" + } +} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.sln b/LearningHub.Nhs.WebUI.sln index 9cce69d55..5aea6885f 100644 --- a/LearningHub.Nhs.WebUI.sln +++ b/LearningHub.Nhs.WebUI.sln @@ -79,6 +79,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LearningHub.Nhs.ReportApi.S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LearningHub.Nhs.ReportApi.Shared", "ReportAPI\LearningHub.Nhs.ReportApi.Shared\LearningHub.Nhs.ReportApi.Shared.csproj", "{6167F037-166C-4C5A-81BE-55618E77D4E8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LearningHub.Nhs.WebUI.AutomatedUiTests", "LearningHub.Nhs.WebUI.AutomatedUiTests\LearningHub.Nhs.WebUI.AutomatedUiTests.csproj", "{A84EC50B-2B01-4819-A2B1-BD867B7595CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -335,6 +337,14 @@ Global {6167F037-166C-4C5A-81BE-55618E77D4E8}.Release|Any CPU.Build.0 = Release|Any CPU {6167F037-166C-4C5A-81BE-55618E77D4E8}.Release|x64.ActiveCfg = Release|Any CPU {6167F037-166C-4C5A-81BE-55618E77D4E8}.Release|x64.Build.0 = Release|Any CPU + {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Debug|x64.Build.0 = Debug|Any CPU + {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Release|Any CPU.Build.0 = Release|Any CPU + {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Release|x64.ActiveCfg = Release|Any CPU + {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE