diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7cf1e24 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,65 @@ +name: E2E Tests + +on: + push: + branches: [ "main" ] + workflow_dispatch: + +jobs: + selenium-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Install Chrome and ChromeDriver + run: | + sudo apt-get update + sudo apt-get install -y wget gnupg unzip + wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list + sudo apt-get update + sudo apt-get install -y google-chrome-stable + + # Install matching ChromeDriver + CHROME_VERSION=$(google-chrome --version | grep -oE "[0-9]+\\.[0-9]+\\.[0-9]+") + CHROMEDRIVER_VERSION=$(curl -s "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_$CHROME_VERSION") + if [ -z "$CHROMEDRIVER_VERSION" ]; then + CHROMEDRIVER_VERSION=$(curl -s "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE") + fi + wget -O /tmp/chromedriver.zip "https://storage.googleapis.com/chrome-for-testing-public/$CHROMEDRIVER_VERSION/linux64/chromedriver-linux64.zip" + unzip /tmp/chromedriver.zip -d /tmp/ + sudo mv /tmp/chromedriver-linux64/chromedriver /usr/local/bin/chromedriver + sudo chmod +x /usr/local/bin/chromedriver + + google-chrome --version + chromedriver --version + + + - name: Verify Chrome install + run: | + chromium-browser --version + chromedriver --version + + - name: Restore dependencies + run: dotnet restore src/ + + - name: Build solution + run: dotnet build src/ --configuration Release --no-restore + + - name: Run Selenium tests + env: + DOTNET_ENVIRONMENT: Development + run: dotnet test src + + - name: Upload test results + uses: actions/upload-artifact@v4 + with: + name: selenium-test-results + path: "**/TestResults/*.trx" \ No newline at end of file diff --git a/README.md b/README.md index d29f80d..c787839 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Berufsschule_HAM ![Build & Publish](https://github.com/LD-Reborn/Berufsschule_HAM/actions/workflows/deploy.yml/badge.svg) +[![E2E Tests](https://github.com/LD-Reborn/Berufsschule_HAM/actions/workflows/test.yml/badge.svg)](https://github.com/LD-Reborn/Berufsschule_HAM/actions/workflows/test.yml) This Project serves the purpose of managing Hardware-Assets diff --git a/tests/Berufsschule_HAM.E2ETests/Berufsschule_HAM.E2ETests.csproj b/tests/Berufsschule_HAM.E2ETests/Berufsschule_HAM.E2ETests.csproj index 9c10485..c3d5cbc 100644 --- a/tests/Berufsschule_HAM.E2ETests/Berufsschule_HAM.E2ETests.csproj +++ b/tests/Berufsschule_HAM.E2ETests/Berufsschule_HAM.E2ETests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Berufsschule_HAM.E2ETests/GroupsPageTests.cs b/tests/Berufsschule_HAM.E2ETests/GroupsPageTests.cs index c280cd6..4942c35 100644 --- a/tests/Berufsschule_HAM.E2ETests/GroupsPageTests.cs +++ b/tests/Berufsschule_HAM.E2ETests/GroupsPageTests.cs @@ -43,12 +43,14 @@ public class GroupsPageTests : IDisposable [Fact] public void GroupsPage_ShouldCreateAndUpdateAndDeleteGroup() { + string randomName = $"RESERVED_TEST_{AppHelper.GetRandomName()}"; + string randomName2 = $"RESERVED_TEST_{AppHelper.GetRandomName()}"; AppHelper.Login(_driver); GroupsHelper.NavigateToGroupsPage(_driver); - GroupsHelper.CreateGroup(_driver, "RESERVED_TEST", "TEST GROUP", true, true, true, true, true, true); - GroupsHelper.UpdateGroup(_driver, "RESERVED_TEST", "RESERVED_TEST2", "TEST GROUP", false, false, false, false, false, false); - GroupsHelper.UpdateGroup(_driver, "RESERVED_TEST2", "RESERVED_TEST", "TEST GROUP", true, true, true, true, true, true); - GroupsHelper.DeleteGroup(_driver, "RESERVED_TEST"); + GroupsHelper.CreateGroup(_driver, randomName, "TEST GROUP", true, true, true, true, true, true); + GroupsHelper.UpdateGroup(_driver, randomName, randomName2, "TEST GROUP", false, false, false, false, false, false); + GroupsHelper.UpdateGroup(_driver, randomName2, randomName, "TEST GROUP", true, true, true, true, true, true); + GroupsHelper.DeleteGroup(_driver, randomName); } public void Dispose() { diff --git a/tests/Berufsschule_HAM.E2ETests/Helper/AppHelper.cs b/tests/Berufsschule_HAM.E2ETests/Helper/AppHelper.cs index 94ec6fd..eaacf45 100644 --- a/tests/Berufsschule_HAM.E2ETests/Helper/AppHelper.cs +++ b/tests/Berufsschule_HAM.E2ETests/Helper/AppHelper.cs @@ -2,6 +2,8 @@ using System.Diagnostics; using System.Net; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Support.UI; +using SeleniumExtras.WaitHelpers; namespace Berufsschule_HAM.E2ETests.Helper; @@ -9,11 +11,12 @@ public static class AppHelper { public const string ServerUrl = "http://localhost:5275"; public static Uri ServerUri = new(ServerUrl); + public static int DefaultTimeout = 5; public static async Task StartApp(string appUrl) { var startInfo = new ProcessStartInfo { - FileName = "/snap/bin/dotnet", + FileName = "dotnet", Arguments = "run --project ../../../../../src/", WorkingDirectory = AppContext.BaseDirectory, UseShellExecute = false, @@ -49,7 +52,10 @@ public static class AppHelper public static ChromeDriver GetChromeDriver() { var options = new ChromeOptions(); - //options.AddArgument("--headless"); + options.AddArgument("--headless"); + options.AddArgument("--no-sandbox"); + options.AddArgument("--disable-dev-shm-usage"); + options.AddArgument("--window-size=1920,1080"); return new ChromeDriver(options); } @@ -68,33 +74,8 @@ public static class AppHelper public static bool TryRetryFindSuccessToast(ChromeDriver driver) { - int retryCounter = 0; - retry: - try - { - - IWebElement successToast = driver.FindElement(By.CssSelector("#toastContainer div.bg-success")); - if (successToast.Displayed) - { - return true; - } - } - catch (Exception) { } - try - { - IWebElement failToast = driver.FindElement(By.CssSelector("#toastContainer div.bg-danger")); - if (failToast.Displayed) - { - return false; - } - } - catch (Exception) { } - if (++retryCounter < 5) - { - Thread.Sleep(250); - goto retry; - } - return false; + Timeout(driver).Until(ExpectedConditions.ElementIsVisible(By.CssSelector("#toastContainer div.bg-success"))); + return true; } public static bool CheckboxGetState(IWebElement webElement) @@ -121,4 +102,24 @@ public static class AppHelper throw; } } + + public static void AwaitVisible(ChromeDriver driver, By by) + { + Timeout(driver).Until(ExpectedConditions.ElementIsVisible(by)); + } + + public static WebDriverWait Timeout(ChromeDriver driver, int timeout) + { + return new(driver, TimeSpan.FromSeconds(timeout)); + } + + public static WebDriverWait Timeout(ChromeDriver driver) + { + return new(driver, TimeSpan.FromSeconds(DefaultTimeout)); + } + + public static string GetRandomName() + { + return new string([.. Enumerable.Range(0, 8).Select(_ => (char)('a' + new Random().Next(26)))]); + } } \ No newline at end of file diff --git a/tests/Berufsschule_HAM.E2ETests/Helper/GroupsHelper.cs b/tests/Berufsschule_HAM.E2ETests/Helper/GroupsHelper.cs index fdb2079..8c9782c 100644 --- a/tests/Berufsschule_HAM.E2ETests/Helper/GroupsHelper.cs +++ b/tests/Berufsschule_HAM.E2ETests/Helper/GroupsHelper.cs @@ -3,6 +3,7 @@ using System.Net; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Support.UI; +using SeleniumExtras.WaitHelpers; namespace Berufsschule_HAM.E2ETests.Helper; @@ -13,7 +14,7 @@ public static class GroupsHelper NavigateToGroupsPage(driver); IWebElement createGroupButton = driver.FindElement(By.CssSelector("button[data-bs-target=\"#createGroupModal\"]")); createGroupButton.Click(); - Thread.Sleep(500); + AppHelper.AwaitVisible(driver, By.Id("cn")); driver.FindElement(By.Id("cn")).SendKeys(cn); driver.FindElement(By.Id("displayname")).SendKeys(displayName); if (canInventorize) driver.FindElement(By.Id("canInventorize")).Click(); @@ -32,7 +33,7 @@ public static class GroupsHelper NavigateToGroupsPage(driver); IWebElement updateGroupButton = driver.FindElement(By.CssSelector($"button[data-group-id=\"{cn}\"].btn-update")); AppHelper.ScrollIntoViewAndClick(driver, updateGroupButton); - Thread.Sleep(500); + AppHelper.AwaitVisible(driver, By.CssSelector("input#groupId")); var cnInput = driver.FindElement(By.CssSelector("input#groupId")); cnInput.Click(); cnInput.Clear(); @@ -69,7 +70,7 @@ public static class GroupsHelper NavigateToGroupsPage(driver); IWebElement deleteButton = driver.FindElement(By.CssSelector($"button[data-group-id=\"{cn}\"].btn-delete")); AppHelper.ScrollIntoViewAndClick(driver, deleteButton); - Thread.Sleep(1000); + AppHelper.AwaitVisible(driver, By.Id("deleteModal")); driver.FindElement(By.Id("deleteModal")); IWebElement deleteConfirmButton = driver.FindElement(By.CssSelector("#deleteModal .btn-danger")); deleteConfirmButton.Click(); diff --git a/tests/Berufsschule_HAM.E2ETests/Helper/LocationsHelper.cs b/tests/Berufsschule_HAM.E2ETests/Helper/LocationsHelper.cs index 6d9f707..666b4de 100644 --- a/tests/Berufsschule_HAM.E2ETests/Helper/LocationsHelper.cs +++ b/tests/Berufsschule_HAM.E2ETests/Helper/LocationsHelper.cs @@ -13,7 +13,7 @@ public static class LocationsHelper NavigateToLocationsPage(driver); IWebElement createLocationButton = driver.FindElement(By.CssSelector("button[data-bs-target=\"#createModal\"]")); createLocationButton.Click(); - Thread.Sleep(500); + AppHelper.AwaitVisible(driver, By.Id("createLocationName")); driver.FindElement(By.Id("createLocationName")).SendKeys(name); driver.FindElement(By.Id("createRoomNumber")).SendKeys(room); driver.FindElement(By.Id("createSeat")).SendKeys(seat); @@ -28,7 +28,7 @@ public static class LocationsHelper string selector = $"button.btn-warning[data-location-name=\"{name}\"][data-room-number=\"{room}\"][data-seat=\"{seat}\"]"; IWebElement updateLocationButton = driver.FindElement(By.CssSelector(selector)); AppHelper.ScrollIntoViewAndClick(driver, updateLocationButton); - Thread.Sleep(500); + AppHelper.AwaitVisible(driver, By.Id("editLocationName")); List ids = [["editLocationName", newName], ["editRoomNumber", newRoom], ["editSeat", newSeat]]; ids.ForEach(id => { @@ -48,7 +48,7 @@ public static class LocationsHelper NavigateToLocationsPage(driver); IWebElement deleteButton = driver.FindElement(By.CssSelector($"button.btn-danger[data-location-id=\"{cn}\"]")); AppHelper.ScrollIntoViewAndClick(driver, deleteButton); - Thread.Sleep(1000); + AppHelper.AwaitVisible(driver, By.Id("deleteModal")); driver.FindElement(By.Id("deleteModal")); IWebElement deleteConfirmButton = driver.FindElement(By.Id("deleteForm")); deleteConfirmButton.Click(); diff --git a/tests/Berufsschule_HAM.E2ETests/HomePageTests.cs b/tests/Berufsschule_HAM.E2ETests/HomePageTests.cs index 6620a6c..25c6a5a 100644 --- a/tests/Berufsschule_HAM.E2ETests/HomePageTests.cs +++ b/tests/Berufsschule_HAM.E2ETests/HomePageTests.cs @@ -1,6 +1,7 @@ using Berufsschule_HAM.E2ETests.Helper; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; +using SeleniumExtras.WaitHelpers; using System.Diagnostics; namespace Berufsschule_HAM.E2ETests; @@ -46,7 +47,7 @@ public class HomePageTests : IDisposable IWebElement logout = _driver.FindElement(By.CssSelector("a[href=\"/Home/Logout\"]")); Assert.True(logout.Displayed); logout.Click(); - Thread.Sleep(250); + AppHelper.Timeout(_driver).Until(ExpectedConditions.UrlContains("/Home/Login")); Assert.Contains("/Home/Login", _driver.Url); } diff --git a/tests/Berufsschule_HAM.E2ETests/InventoryPageTests.cs b/tests/Berufsschule_HAM.E2ETests/InventoryPageTests.cs index 48e1d5c..d82bc48 100644 --- a/tests/Berufsschule_HAM.E2ETests/InventoryPageTests.cs +++ b/tests/Berufsschule_HAM.E2ETests/InventoryPageTests.cs @@ -1,6 +1,7 @@ using Berufsschule_HAM.E2ETests.Helper; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; +using SeleniumExtras.WaitHelpers; using System.Diagnostics; namespace Berufsschule_HAM.E2ETests; @@ -41,11 +42,10 @@ public class InventoryPageTests : IDisposable assetIdInputField.SendKeys("1"); IWebElement scanBarcode = _driver.FindElement(By.Id("enterAssetIdManuallyButton")); scanBarcode.Click(); - Thread.Sleep(500); + AppHelper.AwaitVisible(_driver, By.Id("viewAssetModal")); IWebElement viewModal = _driver.FindElement(By.Id("viewAssetModal")); Assert.True(viewModal.Displayed); Assert.True(_driver.FindElement(By.CssSelector("svg[class=\"form-control\"]")).Displayed); - Thread.Sleep(500); Assert.NotEmpty(_driver.FindElement(By.Id("detailName")).GetAttribute("value") ?? ""); Assert.NotEmpty(_driver.FindElement(By.Id("detailLocation")).GetAttribute("value") ?? ""); @@ -71,12 +71,11 @@ public class InventoryPageTests : IDisposable assetIdInputField.SendKeys("1"); IWebElement scanBarcode = _driver.FindElement(By.Id("enterAssetIdManuallyButton")); scanBarcode.Click(); - Thread.Sleep(500); + AppHelper.AwaitVisible(_driver, By.Id("viewAssetModal")); IWebElement viewModal = _driver.FindElement(By.Id("viewAssetModal")); Assert.True(viewModal.Displayed); IWebElement okButton = _driver.FindElement(By.CssSelector("#viewAssetModal button.btn.btn-primary[data-bs-dismiss=\"modal\"]")); okButton.Click(); - Thread.Sleep(500); Assert.True(AppHelper.TryRetryFindSuccessToast(_driver)); } @@ -89,15 +88,14 @@ public class InventoryPageTests : IDisposable assetIdInputField.SendKeys("1"); IWebElement scanBarcode = _driver.FindElement(By.Id("enterAssetIdManuallyButton")); scanBarcode.Click(); - Thread.Sleep(500); + AppHelper.AwaitVisible(_driver, By.Id("viewAssetModal")); IWebElement viewModal = _driver.FindElement(By.Id("viewAssetModal")); Assert.True(viewModal.Displayed); IWebElement updateButton = _driver.FindElement(By.CssSelector("#viewAssetModal button.btn.btn-warning[data-bs-dismiss=\"modal\"]")); updateButton.Click(); - Thread.Sleep(1000); + AppHelper.AwaitVisible(_driver, By.CssSelector("#updateAssetModal button.btn.btn-warning[type=\"submit\"]")); IWebElement okButton = _driver.FindElement(By.CssSelector("#updateAssetModal button.btn.btn-warning[type=\"submit\"]")); okButton.Click(); - Thread.Sleep(500); Assert.True(AppHelper.TryRetryFindSuccessToast(_driver)); } @@ -108,7 +106,7 @@ public class InventoryPageTests : IDisposable _driver.Navigate().GoToUrl(new Uri(serverUri, "/Home/Inventory")); IWebElement link = _driver.FindElement(By.CssSelector("a[href=\"/Home/Assets?CreateModal=true\"]")); link.Click(); - Thread.Sleep(500); + AppHelper.Timeout(_driver).Until(ExpectedConditions.UrlContains("/Home/Assets")); Assert.Contains("/Home/Assets", _driver.Url); } @@ -119,7 +117,7 @@ public class InventoryPageTests : IDisposable _driver.Navigate().GoToUrl(new Uri(serverUri, "/Home/Inventory")); IWebElement openPrintModalButton = _driver.FindElement(By.Id("openPrintModal")); openPrintModalButton.Click(); - Thread.Sleep(500); + AppHelper.AwaitVisible(_driver, By.Id("printModal")); IWebElement printModal = _driver.FindElement(By.Id("printModal")); Assert.True(printModal.Displayed); } diff --git a/tests/Berufsschule_HAM.E2ETests/LocationsPageTests.cs b/tests/Berufsschule_HAM.E2ETests/LocationsPageTests.cs index 4347766..169ab3b 100644 --- a/tests/Berufsschule_HAM.E2ETests/LocationsPageTests.cs +++ b/tests/Berufsschule_HAM.E2ETests/LocationsPageTests.cs @@ -43,12 +43,14 @@ public class LocationsPageTests : IDisposable [Fact] public void LocationsPage_ShouldCreateAndUpdateAndDeleteGroup() { + string randomName = $"RESERVED_LOCATION_{AppHelper.GetRandomName()}"; + string randomName2 = $"RESERVED_LOCATION_{AppHelper.GetRandomName()}"; AppHelper.Login(_driver); LocationsHelper.NavigateToLocationsPage(_driver); - LocationsHelper.CreateLocation(_driver, "RESERVED_LOCATION", "RESERVED_ROOM", "RESERVED_ROOMNUMBER"); - LocationsHelper.UpdateLocation(_driver, "RESERVED_LOCATION", "RESERVED_ROOM", "RESERVED_ROOMNUMBER", "RESERVED_LOCATION2", "RESERVED_ROOM2", "RESERVED_ROOMNUMBER2"); - LocationsHelper.UpdateLocation(_driver, "RESERVED_LOCATION2", "RESERVED_ROOM2", "RESERVED_ROOMNUMBER2", "RESERVED_LOCATION", "RESERVED_ROOM", "RESERVED_ROOMNUMBER"); - LocationsHelper.DeleteLocation(_driver, "reservedlocation-reservedroom-reservedroomnumber"); + LocationsHelper.CreateLocation(_driver, randomName, "RESERVED_ROOM", "RESERVED_ROOMNUMBER"); + LocationsHelper.UpdateLocation(_driver, randomName, "RESERVED_ROOM", "RESERVED_ROOMNUMBER", randomName2, "RESERVED_ROOM2", "RESERVED_ROOMNUMBER2"); + LocationsHelper.UpdateLocation(_driver, randomName2, "RESERVED_ROOM2", "RESERVED_ROOMNUMBER2", randomName, "RESERVED_ROOM", "RESERVED_ROOMNUMBER"); + LocationsHelper.DeleteLocation(_driver, $"{randomName.ToLower().Replace("_", "")}-reservedroom-reservedroomnumber"); } public void Dispose() {