diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..8af9297 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,167 @@ +name: .NET Reqnroll Automation Tests + +on: + push: + branches: [ "master" ] + workflow_dispatch: + inputs: + test_type: + description: "Select which tests to run" + required: true + default: "All" + type: choice + options: [ "UI", "API", "All" ] + browser: + description: "Select browser" + required: true + default: "Chrome" + type: choice + options: [ "Chrome", "Edge", "Both" ] + +jobs: + build-and-test: + runs-on: windows-latest + + env: + DOTNET_VERSION: "8.0.x" + # Defaults for push (because push has no inputs) + TEST_TYPE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.test_type || 'All' }} + BROWSER: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.browser || 'Chrome' }} + HEADLESS: "true" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: dotnet --info + run: dotnet --info + + - name: Restore + run: dotnet restore + + - name: Build (Release) + run: dotnet build --configuration Release --no-restore + + - name: Run tests (Release) + shell: pwsh + run: | + Write-Host "Running tests for: $env:TEST_TYPE" + Write-Host "Browser: $env:BROWSER" + + # If your framework reads browser from ENV, this will work. + # If not, you can later map this to -- TestRunParameters or appsettings. + $env:BROWSER = "$env:BROWSER" + + if ($env:TEST_TYPE -eq "UI") { + dotnet test .\UI_Automation\UI_Automation.csproj --configuration Release --no-build ` + --logger "trx;LogFileName=ui-tests.trx" + } + elseif ($env:TEST_TYPE -eq "API") { + dotnet test .\API_Automation\API_Automation.csproj --configuration Release --no-build ` + --logger "trx;LogFileName=api-tests.trx" + } + else { + dotnet test --configuration Release --no-build ` + --logger "trx;LogFileName=all-tests.trx" + } + + # Upload TRX files always (success or fail) + - name: Upload TRX test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: dotnet-trx-results + path: | + **/*.trx + if-no-files-found: warn + + # Collect allure-results from anywhere into one folder + - name: Collect Allure results + if: always() + shell: pwsh + run: | + Write-Host "Collecting allure-results..." + if (Test-Path "allure-results") { Remove-Item "allure-results" -Recurse -Force } + New-Item -ItemType Directory -Path "allure-results" | Out-Null + + $dirs = Get-ChildItem -Path . -Recurse -Directory -Filter "allure-results" | + Where-Object { $_.FullName -notmatch "\\allure-results$" } + + Write-Host "Found allure-results dirs:" + $dirs | ForEach-Object { Write-Host $_.FullName } + + foreach ($d in $dirs) { + Copy-Item -Path (Join-Path $d.FullName "*") -Destination "allure-results" -Recurse -Force -ErrorAction SilentlyContinue + } + + if ((Get-ChildItem "allure-results" -ErrorAction SilentlyContinue | Measure-Object).Count -gt 0) { + "HAS_ALLURE_RESULTS=true" | Out-File -FilePath $env:GITHUB_ENV -Append + Write-Host "HAS_ALLURE_RESULTS=true" + } else { + "HAS_ALLURE_RESULTS=false" | Out-File -FilePath $env:GITHUB_ENV -Append + Write-Host "HAS_ALLURE_RESULTS=false" + } + + # Generate Allure HTML report (no Docker) using Node + allure-commandline + - name: Setup Node + if: always() + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Allure CLI + if: always() + run: npm i -g allure-commandline + + - name: Download previous Allure history + if: always() + uses: dawidd6/action-download-artifact@v3 + continue-on-error: true + with: + workflow: main.yml + name: allure-history + path: allure-results/history + if_no_artifact_found: warn + + + - name: Generate Allure HTML report + if: always() + shell: pwsh + run: | + if ($env:HAS_ALLURE_RESULTS -eq "true") { + if (Test-Path "allure-report") { Remove-Item "allure-report" -Recurse -Force } + allure generate allure-results -o allure-report --clean + Write-Host "Allure report generated." + } + else { + Write-Host "No allure-results found. Skipping report generation." + } + + - name: Upload Allure HTML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-html-report + path: allure-report/** + if-no-files-found: warn + + - name: Upload Allure results + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-results + path: allure-results/** + if-no-files-found: warn + + - name: Save Allure history for next run + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-history + path: allure-report/history + if-no-files-found: warn \ No newline at end of file diff --git a/.gitignore b/.gitignore index e4d9ca5..707f647 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,125 @@ -# Build folders -bin/ -obj/ +################################## +# .NET build artifacts +################################## +**/bin/ +**/obj/ +**/out/ +artifacts/ +publish/ -# Allure outputs -allure-results/ -allure-report/ +################################## +# Test results +################################## +**/TestResults/ +**/*.trx +**/*.coverage +**/*.coveragexml +coverage.opencover.xml +coverage.xml + +################################## +# Allure +################################## +**/allure-results/ +**/allure-report/ +**/allure-history/ + +################################## +# Selenium / Playwright artifacts +################################## +**/Screenshots/ +**/screenshots/ +**/Videos/ +**/videos/ +**/Downloads/ +**/downloads/ +**/*.png +**/*.jpg +**/*.jpeg + +################################## +# Logs +################################## +**/*.log +**/logs/ -# Visual Studio +################################## +# Environment / secrets +################################## +**/.env +**/.env.* +**/appsettings.Local.json +**/appsettings.*.Local.json +**/secrets.json +**/appsettings.Secret.json + +################################## +# IDE / OS +################################## .vs/ +.idea/ +.vscode/ *.user *.suo -*.cache +*.userprefs +*.rsuser +*.swp +.DS_Store +Thumbs.db + +################################## +# NuGet +################################## *.nupkg +.nuget/ +packages/ -# Test results -TestResult*/ -*.trx +################################## +# Rider +################################## +.idea/ +*.sln.iml -# OS junk -Thumbs.db -.DS_Store +################################## +# Resharper +################################## +_ReSharper*/ +*.DotSettings.user -# Logs +################################## +# Node (ako koristiš npx / allure-commandline) +################################## +node_modules/ +npm-debug.log* +yarn-error.log* +package-lock.json + +################################## +# CI / temp +################################## +**/temp/ +**/tmp/ + +################################## +# Misc build files +################################## +project.lock.json +project.fragment.lock.json +project.assets.json *.log -*.tmp +*.vsix + +################################## +# Coverage and profiling +################################## +*.coverage +*.coveragexml +coverage/ + +################################## +# Visual Studio Code workspace settings +################################## +.vscode/* +!.vscode/extensions.json + +# End of file diff --git a/API_Automation/API_Automation.csproj b/API_Automation/API_Automation.csproj index 4b8c6cb..b224d24 100644 --- a/API_Automation/API_Automation.csproj +++ b/API_Automation/API_Automation.csproj @@ -2,21 +2,24 @@ net8.0 + true enable enable + - - - - - - + + + + + + + - - + + @@ -50,7 +53,7 @@ - + PreserveNewest diff --git a/API_Automation/Client/RestApiClient.cs b/API_Automation/Client/RestApiClient.cs index 10d4490..33afdf5 100644 --- a/API_Automation/Client/RestApiClient.cs +++ b/API_Automation/Client/RestApiClient.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; -using RestSharp; -using API_Automation.Constants; +using API_Automation.Constants; using API_Automation.Helpers; using API_Automation.Models.Response; using API_Automation.Utils; +using Newtonsoft.Json; +using RestSharp; namespace API_Automation.Client { @@ -13,14 +13,14 @@ public class RestApiClient : IApiClient public RestApiClient(string baseUrl) { - var timeoutSeconds = ConfigReader.GetTimeout(); var options = new RestClientOptions(baseUrl) { ThrowOnAnyError = false, - MaxTimeout = timeoutSeconds * 1000 // Convert seconds to milliseconds + Timeout = TimeSpan.FromSeconds(timeoutSeconds) }; + _client = new RestClient(options); } diff --git a/API_Automation/Features/ReplaceABook.feature b/API_Automation/Features/ReplaceABook.feature index 6382801..5e454c1 100644 --- a/API_Automation/Features/ReplaceABook.feature +++ b/API_Automation/Features/ReplaceABook.feature @@ -1,6 +1,10 @@ -@api +@api @smoke Feature: Replace a book via Bookstore API + As a registered user + I want to search for available books and manage my book list + So that I can replace a book in my collection with another one using the Bookstore API + Scenario: Verify that a user can replace a book Given A user is created and authorized When I get all books diff --git a/API_Automation/Features/ReplaceABook.feature.cs b/API_Automation/Features/ReplaceABook.feature.cs index f8ede95..5645ac0 100644 --- a/API_Automation/Features/ReplaceABook.feature.cs +++ b/API_Automation/Features/ReplaceABook.feature.cs @@ -21,15 +21,19 @@ namespace API_Automation.Features [global::NUnit.Framework.DescriptionAttribute("Replace a book via Bookstore API")] [global::NUnit.Framework.FixtureLifeCycleAttribute(global::NUnit.Framework.LifeCycle.InstancePerTestCase)] [global::NUnit.Framework.CategoryAttribute("api")] + [global::NUnit.Framework.CategoryAttribute("smoke")] public partial class ReplaceABookViaBookstoreAPIFeature { private global::Reqnroll.ITestRunner testRunner; private static string[] featureTags = new string[] { - "api"}; + "api", + "smoke"}; - private static global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new global::System.Globalization.CultureInfo("en-US"), "Features", "Replace a book via Bookstore API", null, global::Reqnroll.ProgrammingLanguage.CSharp, featureTags, InitializeCucumberMessages()); + private static global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new global::System.Globalization.CultureInfo("en-US"), "Features", "Replace a book via Bookstore API", " As a registered user\r\n I want to search for available books and manage my book" + + " list\r\n So that I can replace a book in my collection with another one using th" + + "e Bookstore API", global::Reqnroll.ProgrammingLanguage.CSharp, featureTags, InitializeCucumberMessages()); #line 1 "ReplaceABook.feature" #line hidden @@ -120,7 +124,7 @@ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, globa global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Verify that a user can replace a book", null, tagsOfScenario, argumentsOfScenario, featureTags, pickleIndex); string[] tagsOfRule = ((string[])(null)); global::Reqnroll.RuleInfo ruleInfo = null; -#line 4 +#line 8 this.ScenarioInitialize(scenarioInfo, ruleInfo); #line hidden if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags))) @@ -130,22 +134,22 @@ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, globa else { await this.ScenarioStartAsync(); -#line 5 +#line 9 await testRunner.GivenAsync("A user is created and authorized", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); #line hidden -#line 6 +#line 10 await testRunner.WhenAsync("I get all books", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); #line hidden -#line 7 +#line 11 await testRunner.AndAsync("I add the first book to user\'s list", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); #line hidden -#line 8 +#line 12 await testRunner.ThenAsync("User has only one book and it matches the added one", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); #line hidden -#line 9 +#line 13 await testRunner.WhenAsync("I replace the book with the second one", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); #line hidden -#line 10 +#line 14 await testRunner.ThenAsync("The user\'s book list contains only the replaced book", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); #line hidden } diff --git a/API_Automation/Helpers/Logger.cs b/API_Automation/Helpers/Logger.cs index 7c27901..e1362b4 100644 --- a/API_Automation/Helpers/Logger.cs +++ b/API_Automation/Helpers/Logger.cs @@ -23,7 +23,7 @@ public static void LogError(string message, Exception ex = null) } } - + public static void LogWarning(string message) { TestContext.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] WARNING: {message}"); diff --git a/API_Automation/ImplicitUsings.cs b/API_Automation/ImplicitUsings.cs index 77e491f..c5abf5a 100644 --- a/API_Automation/ImplicitUsings.cs +++ b/API_Automation/ImplicitUsings.cs @@ -1,2 +1 @@ -global using NUnit; global using Reqnroll; diff --git a/API_Automation/Models/Response/BooksGetResponse.cs b/API_Automation/Models/Response/BooksGetResponse.cs index 69b750a..0a9c915 100644 --- a/API_Automation/Models/Response/BooksGetResponse.cs +++ b/API_Automation/Models/Response/BooksGetResponse.cs @@ -15,26 +15,8 @@ public class Book [JsonProperty("title")] public string Title { get; set; } - [JsonProperty("subTitle")] - public string SubTitle { get; set; } - [JsonProperty("author")] public string Author { get; set; } - - [JsonProperty("publish_date")] - public string PublishDate { get; set; } - - [JsonProperty("publisher")] - public string Publisher { get; set; } - - [JsonProperty("pages")] - public int Pages { get; set; } - - [JsonProperty("description")] - public string Description { get; set; } - - [JsonProperty("website")] - public string Website { get; set; } } } } diff --git a/API_Automation/Models/Response/BooksPostResponse.cs b/API_Automation/Models/Response/BooksPostResponse.cs deleted file mode 100644 index 8aed027..0000000 --- a/API_Automation/Models/Response/BooksPostResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace API_Automation.Models.Response -{ - public class BooksPostResponse - { - [JsonProperty("books")] - public List Books { get; set; } - - public class Book - { - [JsonProperty("isbn")] - public string Isbn { get; set; } - } - } -} diff --git a/API_Automation/Models/Response/CreateUserResponse.cs b/API_Automation/Models/Response/CreateUserResponse.cs index a909132..3d315d3 100644 --- a/API_Automation/Models/Response/CreateUserResponse.cs +++ b/API_Automation/Models/Response/CreateUserResponse.cs @@ -9,9 +9,5 @@ public class CreateUserResponse [JsonProperty("username")] public string Username { get; set; } - - [JsonProperty("books")] - public List Books { get; set; } } } - diff --git a/API_Automation/Models/Response/TokenResponse.cs b/API_Automation/Models/Response/TokenResponse.cs index 223b998..68bd5be 100644 --- a/API_Automation/Models/Response/TokenResponse.cs +++ b/API_Automation/Models/Response/TokenResponse.cs @@ -17,4 +17,3 @@ public class TokenResponse public string Result { get; set; } } } - diff --git a/API_Automation/Models/Response/UserResponse.cs b/API_Automation/Models/Response/UserResponse.cs index 241fa36..8c66dd6 100644 --- a/API_Automation/Models/Response/UserResponse.cs +++ b/API_Automation/Models/Response/UserResponse.cs @@ -17,31 +17,6 @@ public class Book { [JsonProperty("isbn")] public string Isbn { get; set; } - - [JsonProperty("title")] - public string Title { get; set; } - - [JsonProperty("subTitle")] - public string SubTitle { get; set; } - - [JsonProperty("author")] - public string Author { get; set; } - - [JsonProperty("publish_date")] - public string PublishDate { get; set; } - - [JsonProperty("publisher")] - public string Publisher { get; set; } - - [JsonProperty("pages")] - public int Pages { get; set; } - - [JsonProperty("description")] - public string Description { get; set; } - - [JsonProperty("website")] - public string Website { get; set; } } } } - diff --git a/API_Automation/dockerfile b/API_Automation/dockerfile new file mode 100644 index 0000000..8a76b06 --- /dev/null +++ b/API_Automation/dockerfile @@ -0,0 +1,11 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 + +WORKDIR /app +COPY . . + +WORKDIR /app/API_Automation + +RUN dotnet restore +RUN dotnet build -c Release + +ENTRYPOINT ["dotnet", "test", "API_Automation.csproj", "-c", "Release"] diff --git a/Csharp_Automation.sln b/Csharp_Automation.sln new file mode 100644 index 0000000..3215ac4 --- /dev/null +++ b/Csharp_Automation.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API_Automation", "API_Automation\API_Automation.csproj", "{E4562554-D396-4CF4-8575-A9A56340DE1D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UI_Automation", "UI_Automation\UI_Automation.csproj", "{5F6AA054-7E59-4B01-9768-6DB105BF2FA2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E4562554-D396-4CF4-8575-A9A56340DE1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4562554-D396-4CF4-8575-A9A56340DE1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4562554-D396-4CF4-8575-A9A56340DE1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4562554-D396-4CF4-8575-A9A56340DE1D}.Release|Any CPU.Build.0 = Release|Any CPU + {5F6AA054-7E59-4B01-9768-6DB105BF2FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F6AA054-7E59-4B01-9768-6DB105BF2FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F6AA054-7E59-4B01-9768-6DB105BF2FA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F6AA054-7E59-4B01-9768-6DB105BF2FA2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Csharp_Automation.slnx b/Csharp_Automation.slnx new file mode 100644 index 0000000..c3e099c --- /dev/null +++ b/Csharp_Automation.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Csharp_Automation_Task.slnx b/Csharp_Automation_Task.slnx deleted file mode 100644 index 89d801c..0000000 --- a/Csharp_Automation_Task.slnx +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bbf288 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# C# UI + API Test Automation Framework + +Modern **Test Automation Framework** built with **C# (.NET 8)** — combining +**Selenium WebDriver** for UI testing, **RestSharp** for API testing, and **Reqnroll** (SpecFlow-style) for BDD scenarios. +Enhanced with **Allure Reporting** and fully integrated into **GitHub Actions CI/CD**. + +--- + +## ⚙️ Tech Stack + +| Layer | Tools / Libraries | +| ------------- | --------------------- | +| UI Testing | Selenium WebDriver | +| API Testing | RestSharp | +| BDD Framework | Reqnroll + NUnit | +| Reporting | Allure.Reqnroll + TRX | +| Platform | .NET 8.0 | + +--- + +## 🚀 Getting Started + +### 🧩 Prerequisites + +* .NET SDK 8.0+ +* Chrome / Edge / Firefox browser +* Allure Commandline + Install globally: + + ```bash + npm install -g allure-commandline + ``` + +--- + +## ▶️ Run Tests + +You can execute tests from the root or specific project level. + +```bash +# Run all tests +dotnet test + +# Run specific projects +dotnet test UI_Automation/UI_Automation.csproj +dotnet test API_Automation/API_Automation.csproj + +# Run by category/tag (e.g. Smoke) +dotnet test --filter "Category=Smoke" +``` + +> 💡 Tip: Add `--logger "trx;LogFileName=test_results.trx"` to export TRX reports for CI/CD pipelines. + +--- + +## 📊 Allure Report + +After running tests, Allure result files are generated in the `allure-results/` folder. +You can generate and open the HTML report locally with: + +```bash +allure generate allure-results --clean -o allure-report +allure open allure-report +``` + +> ⚠️ Make sure the input folder is **allure-results**, not **allure-report**. + +**Allure integration notes:** + +* Allure.Reqnroll automatically attaches scenario metadata and screenshots. +* Reports include: Feature → Scenario → Steps → Attachments → Logs. +* Works seamlessly with GitHub Actions for artifact uploads. + +--- + +## 💡 Highlights + +* Unified **UI** & **API** automation layers +* Page Object Model (**POM**) for clean, maintainable UI tests +* Reqnroll Hooks for driver, context, and test lifecycle management +* Integrated Allure.Reqnroll for rich BDD reporting +* Supports test categorization (Smoke, Regression, UI, API) +* GitHub Actions pipeline for continuous testing and reporting + +--- + +## 🗁 Project Structure + +```text +CsharpUiApiAutomation +├── UI_Automation/ +│ ├── Features/ +│ ├── Pages/ +│ ├── Steps/ +│ ├── Support/ +│ └── UI_Automation.csproj +│ +├── API_Automation/ +│ ├── Client/ +│ ├── Tests/ +│ ├── Models/ +│ └── API_Automation.csproj +│ +├── .github/ +│ └── workflows/ +│ └── automation-tests.yml +│ +├── CCsharpUiApiAutomation.sln +├── README.md +└── .gitignore +``` + +--- + +## 🔄 CI/CD Integration + +GitHub Actions pipeline automatically: + +* Builds and tests both **UI + API** layers +* Generates **Allure**, **TRX**, and optional HTML reports +* Uploads them as **GitHub Action artifacts** for download + +**Workflow file:** `.github/workflows/automation-tests.yml` + +Example snippet from workflow: + +```yaml +- name: Run Tests + run: dotnet test --logger "trx;LogFileName=test_results.trx" + +- name: Generate Allure Report + run: | + allure generate allure-results --clean -o allure-report + allure open allure-report +``` + +--- + +👤 **Author:** *Srdjan Miljus — QA Automation Engineer* diff --git a/UI_Automation/Features/CopyAndPaste.feature b/UI_Automation/Features/CopyAndPaste.feature new file mode 100644 index 0000000..dc58665 --- /dev/null +++ b/UI_Automation/Features/CopyAndPaste.feature @@ -0,0 +1,54 @@ +@regression +@allure.epic:SteamWebInterface +@allure.feature:CopyPaste +@allure.owner:Srdjan_Miljus +@allure.severity:critical +@allure.tag:regression +@allure.link:https://store.steampowered.com +Feature: Copy and paste + + As a user + I want to search for game and navigate to the official Steam About page + So that I can play games on the Steam platform + +@ui +@allure.story:Copy_to_clipboard +Scenario: Copy + Given I open Store page + When I search for "FIFA" game + Then I should see the first search result "EA SPORTS FC™ 25" + And I should see the second search result "FIFA 22" + When I search for "THE FINALS" game + And I click on the first search result in the search results + Then I should be redirected to the "THE_FINALS" page + And I should see the game name "THE FINALS" from the 1st search result + When I click on Play Game button + And I click on No, I need Steam button + Then I should be redirected to the "about" page + And I should see the Install Steam button is clickable + And I should see that Playing Now gamers status are less than Online gamers status + +@ui +@allure.story:Paste_from_clipboard +Scenario: Paste + Given I open Store page + When I search for "THE FINALS" game + And I click on the first search result in the search results + Then I should be redirected to the "THE_FINALS" page + And I should see the game name "THE FINALS" from the 1st search result + When I click on Play Game button + And I click on No, I need Steam button + Then I should be redirected to the "about" page + And I should see the Install Steam button is clickable + And I should see that Playing Now gamers status are less than Online gamers status + +@ui +@allure.story:Cut_to_clipboard +Scenario: Cut + Given I open Store page + When I search for "THE FINALS" game + And I click on the first search result in the search results + Then I should be redirected to the "THE_FINALS" page + And I should see the game name "THE FINALS" from the 1st search result + When I click on Play Game button + \ No newline at end of file diff --git a/UI_Automation/Features/CopyAndPaste.feature.cs b/UI_Automation/Features/CopyAndPaste.feature.cs new file mode 100644 index 0000000..587b846 --- /dev/null +++ b/UI_Automation/Features/CopyAndPaste.feature.cs @@ -0,0 +1,232 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by Reqnroll (https://reqnroll.net/). +// Reqnroll Version:3.0.0.0 +// Reqnroll Generator Version:3.0.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +using Reqnroll; +namespace UI_Automation.Features +{ + + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Reqnroll", "3.0.0.0")] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::NUnit.Framework.TestFixtureAttribute()] + [global::NUnit.Framework.DescriptionAttribute("Copy and paste")] + [global::NUnit.Framework.FixtureLifeCycleAttribute(global::NUnit.Framework.LifeCycle.InstancePerTestCase)] + [global::NUnit.Framework.CategoryAttribute("regression")] + [global::NUnit.Framework.CategoryAttribute("allure.epic:SteamWebInterface")] + [global::NUnit.Framework.CategoryAttribute("allure.feature:CopyPaste")] + [global::NUnit.Framework.CategoryAttribute("allure.owner:Srdjan_Miljus")] + [global::NUnit.Framework.CategoryAttribute("allure.severity:critical")] + [global::NUnit.Framework.CategoryAttribute("allure.tag:regression")] + [global::NUnit.Framework.CategoryAttribute("allure.link:https://store.steampowered.com")] + public partial class CopyAndPasteFeature + { + + private global::Reqnroll.ITestRunner testRunner; + + private static string[] featureTags = new string[] { + "regression", + "allure.epic:SteamWebInterface", + "allure.feature:CopyPaste", + "allure.owner:Srdjan_Miljus", + "allure.severity:critical", + "allure.tag:regression", + "allure.link:https://store.steampowered.com"}; + + private static global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new global::System.Globalization.CultureInfo("en-US"), "Features", "Copy and paste", " As a user\r\n I want to search for game and navigate to the official Steam About" + + " page\r\n So that I can play games on the Steam platform", global::Reqnroll.ProgrammingLanguage.CSharp, featureTags, InitializeCucumberMessages()); + + [global::NUnit.Framework.OneTimeSetUpAttribute()] + public static async global::System.Threading.Tasks.Task FeatureSetupAsync() + { + } + + [global::NUnit.Framework.OneTimeTearDownAttribute()] + public static async global::System.Threading.Tasks.Task FeatureTearDownAsync() + { + await global::Reqnroll.TestRunnerManager.ReleaseFeatureAsync(featureInfo); + } + + [global::NUnit.Framework.SetUpAttribute()] + public async global::System.Threading.Tasks.Task TestInitializeAsync() + { + testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(featureHint: featureInfo); + try + { + if (((testRunner.FeatureContext != null) + && (testRunner.FeatureContext.FeatureInfo.Equals(featureInfo) == false))) + { + await testRunner.OnFeatureEndAsync(); + } + } + finally + { + if (((testRunner.FeatureContext != null) + && testRunner.FeatureContext.BeforeFeatureHookFailed)) + { + throw new global::Reqnroll.ReqnrollException("Scenario skipped because of previous before feature hook error"); + } + if ((testRunner.FeatureContext == null)) + { + await testRunner.OnFeatureStartAsync(featureInfo); + } + } + } + + [global::NUnit.Framework.TearDownAttribute()] + public async global::System.Threading.Tasks.Task TestTearDownAsync() + { + if ((testRunner == null)) + { + return; + } + try + { + await testRunner.OnScenarioEndAsync(); + } + finally + { + global::Reqnroll.TestRunnerManager.ReleaseTestRunner(testRunner); + testRunner = null; + } + } + + public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, global::Reqnroll.RuleInfo ruleInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo, ruleInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(global::NUnit.Framework.TestContext.CurrentContext); + } + + public async global::System.Threading.Tasks.Task ScenarioStartAsync() + { + await testRunner.OnScenarioStartAsync(); + } + + public async global::System.Threading.Tasks.Task ScenarioCleanupAsync() + { + await testRunner.CollectScenarioErrorsAsync(); + } + + private static global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages InitializeCucumberMessages() + { + return new global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages("Features/CopyAndPaste.feature.ndjson", 5); + } + + [global::NUnit.Framework.TestAttribute()] + [global::NUnit.Framework.DescriptionAttribute("Copy")] + [global::NUnit.Framework.CategoryAttribute("ui")] + [global::NUnit.Framework.CategoryAttribute("allure.story:Copy_to_clipboard")] + public async global::System.Threading.Tasks.Task Copy() + { + string[] tagsOfScenario = new string[] { + "ui", + "allure.story:Copy_to_clipboard"}; + global::System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); + string pickleIndex = "0"; + global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Copy", null, tagsOfScenario, argumentsOfScenario, featureTags, pickleIndex); + string[] tagsOfRule = ((string[])(null)); + global::Reqnroll.RuleInfo ruleInfo = null; + this.ScenarioInitialize(scenarioInfo, ruleInfo); + if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags))) + { + await testRunner.SkipScenarioAsync(); + } + else + { + await this.ScenarioStartAsync(); + await testRunner.GivenAsync("I open Store page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); + await testRunner.WhenAsync("I search for \"FIFA\" game", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.ThenAsync("I should see the first search result \"EA SPORTS FC™ 25\"", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the second search result \"FIFA 22\"", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.WhenAsync("I search for \"THE FINALS\" game", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.AndAsync("I click on the first search result in the search results", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.ThenAsync("I should be redirected to the \"THE_FINALS\" page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the game name \"THE FINALS\" from the 1st search result", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.WhenAsync("I click on Play Game button", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.AndAsync("I click on No, I need Steam button", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.ThenAsync("I should be redirected to the \"about\" page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the Install Steam button is clickable", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.AndAsync("I should see that Playing Now gamers status are less than Online gamers status", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + } + await this.ScenarioCleanupAsync(); + } + + [global::NUnit.Framework.TestAttribute()] + [global::NUnit.Framework.DescriptionAttribute("Paste")] + [global::NUnit.Framework.CategoryAttribute("ui")] + [global::NUnit.Framework.CategoryAttribute("allure.story:Paste_from_clipboard")] + public async global::System.Threading.Tasks.Task Paste() + { + string[] tagsOfScenario = new string[] { + "ui", + "allure.story:Paste_from_clipboard"}; + global::System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); + string pickleIndex = "1"; + global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Paste", null, tagsOfScenario, argumentsOfScenario, featureTags, pickleIndex); + string[] tagsOfRule = ((string[])(null)); + global::Reqnroll.RuleInfo ruleInfo = null; + this.ScenarioInitialize(scenarioInfo, ruleInfo); + if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags))) + { + await testRunner.SkipScenarioAsync(); + } + else + { + await this.ScenarioStartAsync(); + await testRunner.GivenAsync("I open Store page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); + await testRunner.WhenAsync("I search for \"THE FINALS\" game", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.AndAsync("I click on the first search result in the search results", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.ThenAsync("I should be redirected to the \"THE_FINALS\" page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the game name \"THE FINALS\" from the 1st search result", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.WhenAsync("I click on Play Game button", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.AndAsync("I click on No, I need Steam button", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.ThenAsync("I should be redirected to the \"about\" page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the Install Steam button is clickable", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.AndAsync("I should see that Playing Now gamers status are less than Online gamers status", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + } + await this.ScenarioCleanupAsync(); + } + + [global::NUnit.Framework.TestAttribute()] + [global::NUnit.Framework.DescriptionAttribute("Cut")] + [global::NUnit.Framework.CategoryAttribute("ui")] + [global::NUnit.Framework.CategoryAttribute("allure.story:Cut_to_clipboard")] + public async global::System.Threading.Tasks.Task Cut() + { + string[] tagsOfScenario = new string[] { + "ui", + "allure.story:Cut_to_clipboard"}; + global::System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); + string pickleIndex = "2"; + global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Cut", null, tagsOfScenario, argumentsOfScenario, featureTags, pickleIndex); + string[] tagsOfRule = ((string[])(null)); + global::Reqnroll.RuleInfo ruleInfo = null; + this.ScenarioInitialize(scenarioInfo, ruleInfo); + if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags))) + { + await testRunner.SkipScenarioAsync(); + } + else + { + await this.ScenarioStartAsync(); + await testRunner.GivenAsync("I open Store page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); + await testRunner.WhenAsync("I search for \"THE FINALS\" game", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.AndAsync("I click on the first search result in the search results", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.ThenAsync("I should be redirected to the \"THE_FINALS\" page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the game name \"THE FINALS\" from the 1st search result", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.WhenAsync("I click on Play Game button", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + } + await this.ScenarioCleanupAsync(); + } + } +} +#pragma warning restore +#endregion diff --git a/UI_Automation/Features/SearchAndNavigate.feature b/UI_Automation/Features/SearchAndNavigate.feature index 28782a6..964a092 100644 --- a/UI_Automation/Features/SearchAndNavigate.feature +++ b/UI_Automation/Features/SearchAndNavigate.feature @@ -1,17 +1,26 @@ -Feature: SearchAndNavigate +@regression +@allure.epic:SteamWebInterface +@allure.feature:Search +@allure.owner:Srdjan_Miljus +@allure.severity:critical +@allure.tag:regression +@allure.link:https://store.steampowered.com +Feature: Search and navigate As a user I want to search for game and navigate to the official Steam About page So that I can play games on the Steam platform -@regression +@ui +@allure.story:Search_by_game_title +@allure.tag:smoke Scenario: Search for Steam game and navigate to the About page Given I open Store page When I search for "FIFA" game - Then I should see the first search result "FIFA 22" - And I should see the second search result "EA SPORTS FC™ 25" + Then I should see the first search result "EA SPORTS FC™ 25" + And I should see the second search result "FIFA 22" When I search for "THE FINALS" game - When I click on the first search result in the search results + And I click on the first search result in the search results Then I should be redirected to the "THE_FINALS" page And I should see the game name "THE FINALS" from the 1st search result When I click on Play Game button @@ -20,4 +29,27 @@ Scenario: Search for Steam game and navigate to the About page And I should see the Install Steam button is clickable And I should see that Playing Now gamers status are less than Online gamers status +@ui +@allure.story:Navigate_to_About_page +Scenario: Navigate to the About page + Given I open Store page + When I search for "THE FINALS" game + And I click on the first search result in the search results + Then I should be redirected to the "THE_FINALS" page + And I should see the game name "THE FINALS" from the 1st search result + When I click on Play Game button + And I click on No, I need Steam button + Then I should be redirected to the "about" page + And I should see the Install Steam button is clickable + And I should see that Playing Now gamers status are less than Online gamers status +@ui +@allure.story:About_page_verification +Scenario: About page + Given I open Store page + When I search for "THE FINALS" game + And I click on the first search result in the search results + Then I should be redirected to the "THE_FINALS" page + And I should see the game name "THE FINALS" from the 1st search result + When I click on Play Game button + \ No newline at end of file diff --git a/UI_Automation/Features/SearchAndNavigate.feature.cs b/UI_Automation/Features/SearchAndNavigate.feature.cs index bf2c1c9..f9b7427 100644 --- a/UI_Automation/Features/SearchAndNavigate.feature.cs +++ b/UI_Automation/Features/SearchAndNavigate.feature.cs @@ -18,16 +18,30 @@ namespace UI_Automation.Features [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Reqnroll", "3.0.0.0")] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::NUnit.Framework.TestFixtureAttribute()] - [global::NUnit.Framework.DescriptionAttribute("SearchAndNavigate")] + [global::NUnit.Framework.DescriptionAttribute("Search and navigate")] [global::NUnit.Framework.FixtureLifeCycleAttribute(global::NUnit.Framework.LifeCycle.InstancePerTestCase)] + [global::NUnit.Framework.CategoryAttribute("regression")] + [global::NUnit.Framework.CategoryAttribute("allure.epic:SteamWebInterface")] + [global::NUnit.Framework.CategoryAttribute("allure.feature:Search")] + [global::NUnit.Framework.CategoryAttribute("allure.owner:Srdjan_Miljus")] + [global::NUnit.Framework.CategoryAttribute("allure.severity:critical")] + [global::NUnit.Framework.CategoryAttribute("allure.tag:regression")] + [global::NUnit.Framework.CategoryAttribute("allure.link:https://store.steampowered.com")] public partial class SearchAndNavigateFeature { private global::Reqnroll.ITestRunner testRunner; - private static string[] featureTags = ((string[])(null)); + private static string[] featureTags = new string[] { + "regression", + "allure.epic:SteamWebInterface", + "allure.feature:Search", + "allure.owner:Srdjan_Miljus", + "allure.severity:critical", + "allure.tag:regression", + "allure.link:https://store.steampowered.com"}; - private static global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new global::System.Globalization.CultureInfo("en-US"), "Features", "SearchAndNavigate", " As a user\r\n I want to search for game and navigate to the official Steam About" + + private static global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new global::System.Globalization.CultureInfo("en-US"), "Features", "Search and navigate", " As a user\r\n I want to search for game and navigate to the official Steam About" + " page\r\n So that I can play games on the Steam platform", global::Reqnroll.ProgrammingLanguage.CSharp, featureTags, InitializeCucumberMessages()); [global::NUnit.Framework.OneTimeSetUpAttribute()] @@ -103,16 +117,20 @@ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, globa private static global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages InitializeCucumberMessages() { - return new global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages("Features/SearchAndNavigate.feature.ndjson", 3); + return new global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages("Features/SearchAndNavigate.feature.ndjson", 5); } [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Search for Steam game and navigate to the About page")] - [global::NUnit.Framework.CategoryAttribute("regression")] + [global::NUnit.Framework.CategoryAttribute("ui")] + [global::NUnit.Framework.CategoryAttribute("allure.story:Search_by_game_title")] + [global::NUnit.Framework.CategoryAttribute("allure.tag:smoke")] public async global::System.Threading.Tasks.Task SearchForSteamGameAndNavigateToTheAboutPage() { string[] tagsOfScenario = new string[] { - "regression"}; + "ui", + "allure.story:Search_by_game_title", + "allure.tag:smoke"}; global::System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); string pickleIndex = "0"; global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Search for Steam game and navigate to the About page", null, tagsOfScenario, argumentsOfScenario, featureTags, pickleIndex); @@ -128,10 +146,10 @@ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, globa await this.ScenarioStartAsync(); await testRunner.GivenAsync("I open Store page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); await testRunner.WhenAsync("I search for \"FIFA\" game", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); - await testRunner.ThenAsync("I should see the first search result \"FIFA 22\"", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); - await testRunner.AndAsync("I should see the second search result \"EA SPORTS FC™ 25\"", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.ThenAsync("I should see the first search result \"EA SPORTS FC™ 25\"", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the second search result \"FIFA 22\"", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); await testRunner.WhenAsync("I search for \"THE FINALS\" game", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); - await testRunner.WhenAsync("I click on the first search result in the search results", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.AndAsync("I click on the first search result in the search results", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); await testRunner.ThenAsync("I should be redirected to the \"THE_FINALS\" page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); await testRunner.AndAsync("I should see the game name \"THE FINALS\" from the 1st search result", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); await testRunner.WhenAsync("I click on Play Game button", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); @@ -142,6 +160,74 @@ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, globa } await this.ScenarioCleanupAsync(); } + + [global::NUnit.Framework.TestAttribute()] + [global::NUnit.Framework.DescriptionAttribute("Navigate to the About page")] + [global::NUnit.Framework.CategoryAttribute("ui")] + [global::NUnit.Framework.CategoryAttribute("allure.story:Navigate_to_About_page")] + public async global::System.Threading.Tasks.Task NavigateToTheAboutPage() + { + string[] tagsOfScenario = new string[] { + "ui", + "allure.story:Navigate_to_About_page"}; + global::System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); + string pickleIndex = "1"; + global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Navigate to the About page", null, tagsOfScenario, argumentsOfScenario, featureTags, pickleIndex); + string[] tagsOfRule = ((string[])(null)); + global::Reqnroll.RuleInfo ruleInfo = null; + this.ScenarioInitialize(scenarioInfo, ruleInfo); + if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags))) + { + await testRunner.SkipScenarioAsync(); + } + else + { + await this.ScenarioStartAsync(); + await testRunner.GivenAsync("I open Store page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); + await testRunner.WhenAsync("I search for \"THE FINALS\" game", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.AndAsync("I click on the first search result in the search results", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.ThenAsync("I should be redirected to the \"THE_FINALS\" page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the game name \"THE FINALS\" from the 1st search result", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.WhenAsync("I click on Play Game button", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.AndAsync("I click on No, I need Steam button", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.ThenAsync("I should be redirected to the \"about\" page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the Install Steam button is clickable", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.AndAsync("I should see that Playing Now gamers status are less than Online gamers status", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + } + await this.ScenarioCleanupAsync(); + } + + [global::NUnit.Framework.TestAttribute()] + [global::NUnit.Framework.DescriptionAttribute("About page")] + [global::NUnit.Framework.CategoryAttribute("ui")] + [global::NUnit.Framework.CategoryAttribute("allure.story:About_page_verification")] + public async global::System.Threading.Tasks.Task AboutPage() + { + string[] tagsOfScenario = new string[] { + "ui", + "allure.story:About_page_verification"}; + global::System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); + string pickleIndex = "2"; + global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("About page", null, tagsOfScenario, argumentsOfScenario, featureTags, pickleIndex); + string[] tagsOfRule = ((string[])(null)); + global::Reqnroll.RuleInfo ruleInfo = null; + this.ScenarioInitialize(scenarioInfo, ruleInfo); + if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags))) + { + await testRunner.SkipScenarioAsync(); + } + else + { + await this.ScenarioStartAsync(); + await testRunner.GivenAsync("I open Store page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); + await testRunner.WhenAsync("I search for \"THE FINALS\" game", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + await testRunner.AndAsync("I click on the first search result in the search results", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.ThenAsync("I should be redirected to the \"THE_FINALS\" page", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); + await testRunner.AndAsync("I should see the game name \"THE FINALS\" from the 1st search result", ((string)(null)), ((global::Reqnroll.Table)(null)), "And "); + await testRunner.WhenAsync("I click on Play Game button", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); + } + await this.ScenarioCleanupAsync(); + } } } #pragma warning restore diff --git a/UI_Automation/Hooks/AllureStepScreenshotHook.cs b/UI_Automation/Hooks/AllureStepScreenshotHook.cs new file mode 100644 index 0000000..187a238 --- /dev/null +++ b/UI_Automation/Hooks/AllureStepScreenshotHook.cs @@ -0,0 +1,44 @@ +using Allure.Net.Commons; +using OpenQA.Selenium; +using System.Text; + +namespace UI_Automation.Hooks +{ + [Binding] + public class AllureStepScreenshotHook + { + private readonly ScenarioContext _scenarioContext; + + public AllureStepScreenshotHook(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + } + + [AfterStep(Order = 9999)] + public void AfterStep() + { + + if (_scenarioContext.TestError == null) + return; + + if (!_scenarioContext.TryGetValue("driver", out IWebDriver driver) || driver == null) + return; + + + var step = _scenarioContext.StepContext.StepInfo; + var stepText = $"{step.StepDefinitionType} {step.Text}"; + AllureApi.AddAttachment("Failed step", "text/plain", Encoding.UTF8.GetBytes(stepText), ".txt"); + + + AllureApi.AddAttachment("URL", "text/plain", Encoding.UTF8.GetBytes(driver.Url ?? "N/A"), ".txt"); + + + if (driver is ITakesScreenshot ts) + { + var shot = ts.GetScreenshot(); + AllureApi.AddAttachment("Screenshot", "image/png", shot.AsByteArray, ".png"); + } + } + } +} + diff --git a/UI_Automation/Hooks/Hooks.cs b/UI_Automation/Hooks/Hooks.cs deleted file mode 100644 index 0dc5f71..0000000 --- a/UI_Automation/Hooks/Hooks.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Allure.Net.Commons; -using OpenQA.Selenium; - - -namespace UI_Automation.Hooks -{ - [Binding] - public class Hooks - { - private readonly ScenarioContext _scenarioContext; - private readonly IWebDriver _driver; - - public Hooks(ScenarioContext scenarioContext) - { - _scenarioContext = scenarioContext; - - _driver = scenarioContext.ContainsKey("driver") ? (IWebDriver)scenarioContext["driver"] : null; - } - - [BeforeTestRun] - public static void BeforeTestRun() - { - Console.WriteLine("=== Starting UI test run with Allure.Reqnroll ==="); - } - - [AfterStep] - public void AfterStep() - { - if (_scenarioContext.TestError != null && _driver != null) - { - TakeScreenshot("AfterStepFailure"); - } - } - - [AfterScenario] - public void AfterScenario() - { - if (_scenarioContext.TestError != null && _driver != null) - { - TakeScreenshot("AfterScenarioFailure"); - } - - _driver?.Quit(); - } - - private void TakeScreenshot(string namePrefix) - { - try - { - var screenshot = ((ITakesScreenshot)_driver).GetScreenshot(); - var fileName = $"{namePrefix}_{DateTime.Now:yyyyMMdd_HHmmss}.png"; - var path = Path.Combine(Directory.GetCurrentDirectory(), fileName); - - screenshot.SaveAsFile(path); - - AllureApi.AddAttachment(fileName, "image/png", path); - Console.WriteLine($"Screenshot saved and attached: {path}"); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to capture screenshot: {ex.Message}"); - } - } - } -} diff --git a/UI_Automation/Pages/AboutPage.cs b/UI_Automation/Pages/AboutPage.cs index df8dc6b..a665974 100644 --- a/UI_Automation/Pages/AboutPage.cs +++ b/UI_Automation/Pages/AboutPage.cs @@ -1,32 +1,59 @@ using OpenQA.Selenium; +using OpenQA.Selenium.Support.UI; +using SeleniumExtras.WaitHelpers; + namespace UI_Automation.Pages { public class AboutPage { private readonly IWebDriver _driver; + private readonly WebDriverWait _wait; + + #region Locators - // Locators - private IWebElement InstallSteamButton => _driver.FindElement(By.CssSelector("#about_greeting .about_install_steam_link")); - private IList OnlineStatus => _driver.FindElements(By.XPath("//div[contains(@class,'gamers_online')]/parent::div")); - private IList PlayingNowStatus => _driver.FindElements(By.XPath("//div[contains(@class,'gamers_in_game')]/parent::div")); + private readonly By InstallSteamButtonLocator = By.CssSelector("#about_greeting .about_install_steam_link"); + private readonly By OnlineStatusLocator = By.XPath("//div[contains(@class,'gamers_online')]/parent::div"); + private readonly By PlayingNowStatusLocator = By.XPath("//div[contains(@class,'gamers_in_game')]/parent::div"); + #endregion public AboutPage(IWebDriver driver) { _driver = driver; + _wait = new WebDriverWait(driver, TimeSpan.FromSeconds(20)); } public bool IsInstallSteamButtonClickable() { - return InstallSteamButton.Displayed && InstallSteamButton.Enabled; + var button = _wait.Until(ExpectedConditions.ElementToBeClickable(InstallSteamButtonLocator)); + return button.Displayed && button.Enabled; } public bool CompareIfPlayingNowStatusIsLessThanOnlineStatus() { - int onlineStatus = int.Parse(OnlineStatus.Last().Text.Replace("ONLINE", "").Replace(",", "")); - int playingNowStatus = int.Parse(PlayingNowStatus.Last().Text.Replace("PLAYING NOW", "").Replace(",", "")); + var js = (IJavaScriptExecutor)_driver; + + // Scroll to the bottom so the stats section loads into view + js.ExecuteScript("window.scrollTo(0, document.body.scrollHeight);"); + + // Wait for both stat elements to appear and have non-empty text + _wait.Until(d => + { + var online = d.FindElements(OnlineStatusLocator); + var playing = d.FindElements(PlayingNowStatusLocator); + return online.Count > 0 && playing.Count > 0 + && !string.IsNullOrWhiteSpace(online.Last().Text) + && !string.IsNullOrWhiteSpace(playing.Last().Text); + }); + + var onlineElements = _driver.FindElements(OnlineStatusLocator); + var playingNowElements = _driver.FindElements(PlayingNowStatusLocator); + + int onlineStatus = int.Parse(onlineElements.Last().Text.Replace("ONLINE", "").Replace(",", "").Trim()); + int playingNowStatus = int.Parse(playingNowElements.Last().Text.Replace("PLAYING NOW", "").Replace(",", "").Trim()); return onlineStatus > playingNowStatus; } } + } diff --git a/UI_Automation/Pages/StorePage.cs b/UI_Automation/Pages/StorePage.cs index 7c46d98..2a3cdb4 100644 --- a/UI_Automation/Pages/StorePage.cs +++ b/UI_Automation/Pages/StorePage.cs @@ -1,4 +1,5 @@ using OpenQA.Selenium; +using OpenQA.Selenium.Support.UI; namespace UI_Automation.Pages { @@ -6,74 +7,116 @@ public class StorePage { private readonly IWebDriver _driver; private readonly IJavaScriptExecutor _jsExecutor; + private readonly WebDriverWait _wait; - // Locators - private IWebElement SearchBox => _driver.FindElement(By.XPath("//input[@class='_2tlUAG6WNyYFlk9caIiLj5']")); - private IList SearchResults => _driver.FindElements(By.CssSelector(".search_result_row")); - private IWebElement GameNameHeading => _driver.FindElement(By.Id("appHubAppName")); - private IWebElement PlayGameButton => _driver.FindElement(By.Id("freeGameBtn")); - private IWebElement NoINeedSteamButton => _driver.FindElement(By.XPath("//h3[contains(text(),'No, I need Steam')]")); + #region Locators (By) + private readonly By SearchBoxInput = By.XPath("//input[@class='_2tlUAG6WNyYFlk9caIiLj5']"); + private readonly By SearchResultsRows = By.CssSelector(".search_result_row"); + private readonly By GameNameHeading = By.Id("appHubAppName"); + private readonly By PlayGameButton = By.Id("freeGameBtn"); + private readonly By NoINeedSteamButton = By.XPath("//h3[contains(text(),'No, I need Steam')]"); + + #endregion public StorePage(IWebDriver driver) { _driver = driver; _jsExecutor = (IJavaScriptExecutor)driver; + _wait = new WebDriverWait(driver, TimeSpan.FromSeconds(20)); } - // Actions + #region Helpers + + private IWebElement WaitVisible(By by) + => _wait.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.ElementIsVisible(by)); + + private IWebElement WaitClickable(By by) + => _wait.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.ElementToBeClickable(by)); + + private IReadOnlyCollection WaitForSearchResults(int minCount = 1) + { + _wait.Until(d => + { + var els = d.FindElements(SearchResultsRows); + return els != null && els.Count >= minCount; + }); + + return _driver.FindElements(SearchResultsRows); + } + + private void ScrollTo(By by, int yShift = 350) + { + var el = WaitVisible(by); + var linkYPositionShift = el.Location.Y - yShift; + _jsExecutor.ExecuteScript("window.scrollBy(0, arguments[0]);", linkYPositionShift); + } + + #endregion + + #region Actions public void SearchForGame(string gameName) { - SearchBox.Clear(); - SearchBox.SendKeys(gameName); - SearchBox.SendKeys(Keys.Enter); + var search = WaitVisible(SearchBoxInput); + search.Clear(); + search.SendKeys(gameName); + search.SendKeys(Keys.Enter); + + // wait that results load (prevents "Sequence contains no elements") + WaitForSearchResults(1); } public string GetFirstSearchResultText() { - return SearchResults.First().Text; + var results = WaitForSearchResults(1); + return results.First().Text; } public string GetSecondSearchResultText() { - return SearchResults.Count > 1 ? SearchResults[1].Text : string.Empty; + var results = WaitForSearchResults(2); + return results.Skip(1).First().Text; + } + + public void ClickFirstSearchResult() + { + var results = WaitForSearchResults(1); + results.First().Click(); } public void ClickFirstSearchResultWithJs() { - var firstResult = SearchResults.FirstOrDefault(); - if (firstResult != null) - { - _jsExecutor.ExecuteScript("arguments[0].click();", firstResult); - } + var results = WaitForSearchResults(1); + var first = results.First(); + _jsExecutor.ExecuteScript("arguments[0].click();", first); } - public string GetPageUrl() + public void WaitForGameDetailsPage() { - return _driver.Url; + // Steam page sometimes loads slower in Grid/headless + WaitVisible(GameNameHeading); } + public string GetPageUrl() => _driver.Url; + public string GetGameNameHeadingText() { - return GameNameHeading.Text; + WaitForGameDetailsPage(); + return WaitVisible(GameNameHeading).Text; } public void ClickPlayGameButton() { - ScrollToElement(PlayGameButton); - PlayGameButton.Click(); + ScrollTo(PlayGameButton); + WaitClickable(PlayGameButton).Click(); } public void ClickNoINeedSteamButton() { - NoINeedSteamButton.Click(); + WaitClickable(NoINeedSteamButton).Click(); } - private void ScrollToElement(IWebElement element) - { - var linkYPositionShift = element.Location.Y - 350; - _jsExecutor.ExecuteScript("window.scrollBy(0," + linkYPositionShift + ");"); - } + #endregion } } diff --git a/UI_Automation/Reqnroll/userid b/UI_Automation/Reqnroll/userid new file mode 100644 index 0000000..bd09a63 --- /dev/null +++ b/UI_Automation/Reqnroll/userid @@ -0,0 +1 @@ +fc99c5a2-452d-4e45-bb2c-378d19af1108 \ No newline at end of file diff --git a/UI_Automation/Setup/TestSetup.cs b/UI_Automation/Setup/TestSetup.cs index 1954500..3d0393b 100644 --- a/UI_Automation/Setup/TestSetup.cs +++ b/UI_Automation/Setup/TestSetup.cs @@ -1,35 +1,35 @@ -using Allure.Net.Commons; +using Allure.Net.Commons; using NUnit.Framework; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Edge; using OpenQA.Selenium.Firefox; +using OpenQA.Selenium.Remote; using Reqnroll.BoDi; using System.Runtime.InteropServices; +using System.Text; using UI_Automation.Enums; using UI_Automation.Support; +//[assembly: Parallelizable(ParallelScope.Self | ParallelScope.Children)] +//[assembly: Parallelizable(ParallelScope.Fixtures)] +//[assembly: Parallelizable(ParallelScope.All)] +//[assembly: LevelOfParallelism(2)] + namespace UI_Automation.Setup { [Binding] - public class TestSetup : IDisposable { private readonly IObjectContainer _container; - private readonly IScenarioContext _scenarioContext; + private readonly ScenarioContext _scenarioContext; private IWebDriver _driver; private static Config _config; public TestSetup(IObjectContainer container, ScenarioContext scenarioContext) { _container = container; - _scenarioContext = _container.Resolve(); - } - - [OneTimeSetUp] - public void Init() - { - Environment.CurrentDirectory = Path.GetDirectoryName(GetType().Assembly.Location); + _scenarioContext = scenarioContext; } [BeforeTestRun] @@ -37,48 +37,135 @@ public static void GlobalSetup() { _config = Config.Load(); AllureLifecycle.Instance.CleanupResultDirectory(); + + var resultsDir = AllureLifecycle.Instance.ResultsDirectory + ?? Path.Combine(AppContext.BaseDirectory, "allure-results"); + Directory.CreateDirectory(resultsDir); + + + var osName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows" : + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "macOS" : "Linux"; + + File.WriteAllLines(Path.Combine(resultsDir, "environment.properties"), new[] + { + "env=QA", + $"baseUrl={_config.BaseUrl}", + $"browser={_config.Browser}", + $"os={osName}", + "framework=Reqnroll + NUnit + Selenium" + }); + + File.WriteAllText(Path.Combine(resultsDir, "executor.json"), + """ + { + "name": "Local Run", + "type": "other", + "buildName": "UI_Automation" + } + """, Encoding.UTF8); + + File.WriteAllText(Path.Combine(resultsDir, "categories.json"), + """ + [ + { + "name": "Product defects", + "matchedStatuses": ["failed"] + }, + { + "name": "Test defects", + "matchedStatuses": ["broken"] + }, + { + "name": "Ignored tests", + "matchedStatuses": ["skipped"] + } + ] + """, Encoding.UTF8); } - [BeforeScenario] public void InitializeWebDriver() { - _driver = CreateWebDriver(_config.Browser, _config.Headless, _config.Incognito); - _driver.Manage().Window.Maximize(); - _driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10); - + // Remote toggle (Docker/Grid) + var useRemote = IsRemoteEnabled(); + + _driver = CreateWebDriver( + _config.Browser, + _config.Headless, + _config.Incognito, + useRemote + ); + + try { _driver.Manage().Window.Maximize(); } catch { } + + _container.RegisterInstanceAs(_driver); - Logger.Log($"Initialized WebDriver for browser: {_config.Browser}, Headless: {_config.Headless}, Incognito: {_config.Incognito}"); + _scenarioContext["driver"] = _driver; + + Logger.Log($"Initialized WebDriver for browser: {_config.Browser}, Headless: {_config.Headless}, Incognito: {_config.Incognito}, Remote: {useRemote}"); } - private static IWebDriver CreateWebDriver(string browserName, bool headless, bool incognito) + private static bool IsRemoteEnabled() { + var raw = Environment.GetEnvironmentVariable("USE_REMOTE") ?? "false"; + return raw.Equals("true", StringComparison.OrdinalIgnoreCase) || + raw.Equals("1", StringComparison.OrdinalIgnoreCase) || + raw.Equals("yes", StringComparison.OrdinalIgnoreCase); + } + + private static string GetRemoteUrl() + { + // docker-compose: http://selenium:4444/wd/hub + // local host: http://localhost:4444/wd/hub + return Environment.GetEnvironmentVariable("SELENIUM_REMOTE_URL") + ?? "http://selenium:4444/wd/hub"; + + } + + private static IWebDriver CreateWebDriver(string browserName, bool headless, bool incognito, bool useRemote) + { + //If remote is enabled → use Selenium Grid (docker chrome) + if (useRemote) + { + var remoteUrl = GetRemoteUrl(); + + // Remote side is typically Chrome in selenium/standalone-chrome + // Keep options aligned with your existing flags. + var options = new ChromeOptions(); + if (headless) options.AddArgument("--headless=new"); + if (incognito) options.AddArgument("--incognito"); + + // Optional stability options (safe for Docker) + options.AddArgument("--no-sandbox"); + options.AddArgument("--disable-dev-shm-usage"); + options.AddArgument("--window-size=1920,1080"); + + return new RemoteWebDriver(new Uri(remoteUrl), options); + } + if (Enum.TryParse(browserName, true, out BrowserType browser)) { switch (browser) { case BrowserType.Chrome: var chromeOptions = new ChromeOptions(); - if (headless) - chromeOptions.AddArgument("--headless=new"); - if (incognito) - chromeOptions.AddArgument("--incognito"); + if (headless) chromeOptions.AddArgument("--headless=new"); + if (incognito) chromeOptions.AddArgument("--incognito"); + chromeOptions.AddArgument("--no-sandbox"); + chromeOptions.AddArgument("--disable-dev-shm-usage"); + chromeOptions.AddArgument("--window-size=1920,1080"); return new ChromeDriver(chromeOptions); case BrowserType.Firefox: var firefoxOptions = new FirefoxOptions(); - if (headless) - firefoxOptions.AddArgument("--headless"); - if (incognito) - firefoxOptions.AddArgument("-private"); + if (headless) firefoxOptions.AddArgument("--headless"); + if (incognito) firefoxOptions.AddArgument("-private"); return new FirefoxDriver(firefoxOptions); case BrowserType.Edge: var edgeOptions = new EdgeOptions(); - if (headless) - edgeOptions.AddArgument("headless"); - if (incognito) - edgeOptions.AddArgument("inprivate"); + if (headless) edgeOptions.AddArgument("headless"); + if (incognito) edgeOptions.AddArgument("inprivate"); return new EdgeDriver(edgeOptions); case BrowserType.Safari: @@ -91,23 +178,26 @@ private static IWebDriver CreateWebDriver(string browserName, bool headless, boo return new OpenQA.Selenium.Safari.SafariDriver(); } } - throw new ArgumentOutOfRangeException(nameof(browser), $"Browser '{browser}' is not supported."); + + throw new ArgumentOutOfRangeException(nameof(browserName), $"Browser '{browserName}' is not supported."); } public void NavigateToBaseUrl() { - string baseUrl = _config.BaseUrl ?? "https://store.steampowered.com/"; _driver.Navigate().GoToUrl(baseUrl); } - [AfterScenario] - + [AfterScenario(Order = -10000)] public void Dispose() { - if (_driver != null) + if (_driver == null) return; + + try { _driver.Quit(); } + catch { } + finally { - _driver.Quit(); + try { _driver.Dispose(); } catch { } _driver = null; } } diff --git a/UI_Automation/StepDefinitions/SearchAndNavigateStepDefinitions.cs b/UI_Automation/StepDefinitions/SearchAndNavigateStepDefinitions.cs index d0e0961..a447749 100644 --- a/UI_Automation/StepDefinitions/SearchAndNavigateStepDefinitions.cs +++ b/UI_Automation/StepDefinitions/SearchAndNavigateStepDefinitions.cs @@ -19,22 +19,22 @@ public class SearchAndNavigateStepDefinitions public SearchAndNavigateStepDefinitions(IWebDriver driver, StorePage storePage, AboutPage aboutPage, ScenarioContext scenarioContext) { _driver = driver; - _wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10)); + _wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(15)); _config = Config.Load(); _storePage = storePage; _aboutPage = aboutPage; } [Given("I open Store page")] - + public void GivenIOpenStorePage() { - _driver.Navigate().GoToUrl(_config.BaseUrl); + _driver.Navigate().GoToUrl(_config.BaseUrl); Logger.Log("Navigated to Steam Store page: " + _config.BaseUrl); } [When("I search for {string} game")] - + public void WhenISearchForGame(string gameName) { _storePage.SearchForGame(gameName); @@ -42,7 +42,7 @@ public void WhenISearchForGame(string gameName) } [Then("I should see the first search result {string}")] - + public void ThenIShouldSeeTheFirstSearchResult(string searchedFirstGame) { Assert.That(_storePage.GetFirstSearchResultText(), Does.Contain(searchedFirstGame), "Expected the first search result to not be null or empty"); @@ -50,7 +50,7 @@ public void ThenIShouldSeeTheFirstSearchResult(string searchedFirstGame) } [Then("I should see the second search result {string}")] - + public void ThenIShouldSeeTheSecondSearchResult(string searchedSecondGame) { Assert.That(_storePage.GetSecondSearchResultText(), Does.Contain(searchedSecondGame), "Expected the second search result to not be null or empty"); @@ -58,7 +58,7 @@ public void ThenIShouldSeeTheSecondSearchResult(string searchedSecondGame) } [When("I click on the first search result in the search results")] - + public void WhenIClickOnTheFirstSearchResultInTheSearchResults() { _storePage.ClickFirstSearchResultWithJs(); @@ -66,17 +66,25 @@ public void WhenIClickOnTheFirstSearchResultInTheSearchResults() } [Then("I should be redirected to the {string} page")] - public void ThenIShouldBeRedirectedToThePage(string pageUrl) { + try + { + _wait.Until(d => d.Url.Contains(pageUrl)); + } + catch (WebDriverTimeoutException ex) + { + Assert.Fail($"Expected to be redirected to URL containing '{pageUrl}' within 10s. " + + $"Actual URL was '{_driver.Url}'. Timeout: {ex.Message}"); + } - _wait.Until(driver => driver.Url.Contains(pageUrl)); - Assert.That(_storePage.GetPageUrl(), Does.Contain(pageUrl), $"Expected to be redirected to the page containing '{pageUrl}'"); + Assert.That(_driver.Url, Does.Contain(pageUrl), + $"Expected to be redirected to the page containing '{pageUrl}'"); Logger.Log($"Verified redirection to the page: {pageUrl}"); } [Then("I should see the game name {string} from the 1st search result")] - + public void ThenIShouldSeeTheGameNameFromTheStSearchResult(string gameName) { Assert.That(_storePage.GetGameNameHeadingText(), Does.Contain(gameName), "Expected the game name heading text to not be null or empty"); @@ -84,7 +92,7 @@ public void ThenIShouldSeeTheGameNameFromTheStSearchResult(string gameName) } [When("I click on Play Game button")] - + public void WhenIClickOnPlayGameButton() { _storePage.ClickPlayGameButton(); @@ -92,7 +100,7 @@ public void WhenIClickOnPlayGameButton() } [When("I click on No, I need Steam button")] - + public void WhenIClickOnNoINeedSteamButton() { _storePage.ClickNoINeedSteamButton(); @@ -100,7 +108,7 @@ public void WhenIClickOnNoINeedSteamButton() } [Then("I should see the Install Steam button is clickable")] - + public void ThenIShouldSeeTheInstallSteamButtonIsClickable() { Assert.That(_aboutPage.IsInstallSteamButtonClickable(), Is.True, "Expected the Install Steam button to be clickable"); @@ -108,7 +116,7 @@ public void ThenIShouldSeeTheInstallSteamButtonIsClickable() } [Then("I should see that Playing Now gamers status are less than Online gamers status")] - + public void ThenIShouldSeeThatPlayingNowGamersStatusAreLessThanOnlineGamersStatus() { Assert.That(_aboutPage.CompareIfPlayingNowStatusIsLessThanOnlineStatus(), Is.True, "Expected to find at least one Online status element"); diff --git a/UI_Automation/Support/Config.cs b/UI_Automation/Support/Config.cs index 6d4e40c..f90b8a1 100644 --- a/UI_Automation/Support/Config.cs +++ b/UI_Automation/Support/Config.cs @@ -3,33 +3,35 @@ namespace UI_Automation.Support { - public class Config { public string BaseUrl { get; set; } public string Browser { get; set; } - public bool Headless { get; set; } = true; + public bool Headless { get; set; } = true; public bool Incognito { get; set; } = true; + public bool UseRemote { get; set; } = false; + public string RemoteUrl { get; set; } = "http://localhost:4444/wd/hub"; - private static Config _instance; private static readonly object _lock = new(); - [JsonConstructor] - public Config(string baseUrl, string browser, bool headless = true, bool incognito = true) + public Config( + string baseUrl, + string browser, + bool headless = true, + bool incognito = true, + bool useRemote = false, + string remoteUrl = "http://localhost:4444/wd/hub") { BaseUrl = baseUrl; Browser = browser; Headless = headless; Incognito = incognito; + UseRemote = useRemote; + RemoteUrl = remoteUrl; } - /// - /// Loads configuration from appsettings.json and returns a singleton instance. - /// - /// The name of the settings file (default: "appsettings.json"). - /// Config instance with properties set from the file. public static Config Load(string fileName = "appsettings.json") { if (_instance != null) @@ -47,11 +49,25 @@ public static Config Load(string fileName = "appsettings.json") throw new FileNotFoundException($"Configuration file '{fileName}' not found at '{filePath}'."); var json = File.ReadAllText(filePath); - _instance = JsonSerializer.Deserialize(json); + + _instance = JsonSerializer.Deserialize(json, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); if (_instance == null) throw new InvalidOperationException("Failed to deserialize configuration."); + // Allow environment variable overrides for CI + var envBrowser = Environment.GetEnvironmentVariable("BROWSER"); + if (!string.IsNullOrWhiteSpace(envBrowser)) + _instance.Browser = envBrowser; + + var envHeadless = Environment.GetEnvironmentVariable("HEADLESS"); + if (!string.IsNullOrWhiteSpace(envHeadless)) + _instance.Headless = envHeadless.Equals("true", StringComparison.OrdinalIgnoreCase); + return _instance; } } diff --git a/UI_Automation/UI_Automation.csproj b/UI_Automation/UI_Automation.csproj index 3013db8..9157f33 100644 --- a/UI_Automation/UI_Automation.csproj +++ b/UI_Automation/UI_Automation.csproj @@ -9,28 +9,30 @@ + - - - + + + + - - + + - - - + + + - + - - - + + + @@ -53,4 +55,9 @@ + + + + + diff --git a/UI_Automation/allureConfig.json b/UI_Automation/allureConfig.json index f97e274..4d31b0d 100644 --- a/UI_Automation/allureConfig.json +++ b/UI_Automation/allureConfig.json @@ -1,6 +1,6 @@ { - "$schema": "https://raw.githubusercontent.com/allure-framework/allure-csharp/2.14.1/Allure.Reqnroll/Schemas/allureConfig.schema.json", "allure": { - "directory": "allure-results" + "resultsDirectory": "allure-results", + "reportDirectory": "allure-report" } } diff --git a/UI_Automation/appsettings.json b/UI_Automation/appsettings.json index ce4e29b..85a3cf5 100644 --- a/UI_Automation/appsettings.json +++ b/UI_Automation/appsettings.json @@ -1,6 +1,9 @@ { - "Headless": false, + "Headless": true, "Incognito": true, "Browser": "Chrome", - "BaseUrl": "https://store.steampowered.com" + "BaseUrl": "https://store.steampowered.com", + + "UseRemote": false, + "RemoteUrl": "http://localhost:4444/wd/hub" } diff --git a/UI_Automation/dockerfile b/UI_Automation/dockerfile new file mode 100644 index 0000000..a5c4abd --- /dev/null +++ b/UI_Automation/dockerfile @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 + +WORKDIR /app +COPY . . + +WORKDIR /app/UI_Automation + +RUN dotnet restore +RUN dotnet build -c Release + +ENV USE_REMOTE=true +ENV SELENIUM_REMOTE_URL=http://selenium:4444/wd/hub + +ENTRYPOINT ["dotnet", "test", "UI_Automation.csproj", "-c", "Release"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..09f81cd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +name: csharp-automation-task + +services: + selenium: + image: selenium/standalone-chrome:latest + shm_size: 2gb + ports: + - "4444:4444" + environment: + - SE_NODE_MAX_SESSIONS=2 + - SE_NODE_OVERRIDE_MAX_SESSIONS=true + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:4444/status"] + interval: 5s + timeout: 5s + retries: 20 + + ui-tests: + build: + context: . + dockerfile: UI_Automation/Dockerfile + image: csharp-automation-task-ui-tests:latest + depends_on: + selenium: + condition: service_healthy + environment: + - USE_REMOTE=true + - SELENIUM_REMOTE_URL=http://selenium:4444/wd/hub + + api-tests: + build: + context: . + dockerfile: API_Automation/Dockerfile + image: csharp-automation-task-api-tests:latest diff --git a/global.json b/global.json new file mode 100644 index 0000000..1406adc --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.417", + "rollForward": "latestMinor" + } +}