diff --git a/.github/workflows/crossBrowserTesting.yml b/.github/workflows/crossBrowserTesting.yml new file mode 100644 index 00000000..4fe2994a --- /dev/null +++ b/.github/workflows/crossBrowserTesting.yml @@ -0,0 +1,134 @@ +name: Cross-browser testing + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: "27 20 * * 0" + +jobs: + + # Summary: + # + # * Installs and configures the environment + # * Builds the solution in Debug configuration + # * Runs just the Selenium tests + # * In Debug configuration (.NET tests) + # * Using the BrowserStack browser configuration + + browser_tests: + + strategy: + max-parallel: 1 + fail-fast: false + matrix: + include: + - browserName: Chrome + browserVersion: latest-50 + os: Windows + osVersion: 11 + - browserName: Chrome + browserVersion: latest + os: Windows + osVersion: 11 + - browserName: Edge + browserVersion: latest + os: Windows + osVersion: 11 + - browserName: Firefox + browserVersion: latest + os: Windows + osVersion: 11 + - browserName: Firefox + browserVersion: latest-40 + os: Windows + osVersion: 11 + - browserName: Safari + browserVersion: 17.3 + os: OS X + osVersion: Sonoma + - browserName: Safari + browserVersion: 26.2 + os: OS X + osVersion: Tahoe + + name: Run tests + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + env: + RunNumber: ${{ github.run_number }}.${{ github.run_attempt }} + VersionSuffix: crossbrowser.${{ github.run_number }} + Configuration: Release + Tfm: net8.0 + DotnetVersion: 8.0.x + WebDriverFactory__SelectedConfiguration: BrowserStack + BSbrowserName: ${{ matrix.browserName }} + BSbrowserVersion: ${{ matrix.browserVersion }} + BSos: ${{ matrix.os }} + BSosVersion: ${{ matrix.osVersion }} + BSuserName: ${{ secrets.BROWSERSTACK_USERNAME }} + BSaccessKey: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + BSprojectName: CSF.Screenplay + BSbuildName: ghActionsRun.${{ github.run_number }}.${{ github.run_attempt }}_${{ matrix.browserName }}:${{ matrix.browserVersion }}_${{ matrix.os }}:${{ matrix.osVersion }} + BSparallelism: 5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Install build dependencies + + - name: Install .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DotnetVersion }} + - name: Install Node.js for building JSON-to-HTML report converter + uses: actions/setup-node@v6.2.0 + + # Environment setup pre-build + + - name: Restore .NET packages + run: dotnet restore + - name: Restore Node modules + run: | + cd CSF.Screenplay.JsonToHtmlReport.Template/src + npm ci + cd ../.. + + # Build and test the solution + + - name: Build the solution + run: dotnet build -c ${{ env.Configuration }} + - name: Run .NET Selenium tests only + id: dotnet_tests + run: dotnet test -c ${{ env.Configuration }} --no-build Tests/CSF.Screenplay.Selenium.Tests -- NumberOfTestWorkers=$BSparallelism + continue-on-error: true + + # Post-test tasks (artifacts, overall status) + - name: Upload Screenplay JSON report artifact + uses: actions/upload-artifact@v4 + with: + name: Screenplay JSON reports ${{ matrix.browserName }}_${{ matrix.browserVersion }}_${{ matrix.os }}_${{ matrix.osVersion }} + path: Tests/CSF.Screenplay.Selenium.Tests/**/ScreenplayReport_*.json + - name: Convert Screenplay reports to HTML + continue-on-error: true + run: | + for report in $(find Tests/CSF.Screenplay.Selenium.Tests/ -type f -name "ScreenplayReport_*.json") + do + reportDir=$(dirname "$report") + outputFile="$reportDir/ScreenplayReport.html" + dotnet run --no-build --framework $Tfm -c ${{ env.Configuration }} --project CSF.Screenplay.JsonToHtmlReport --ReportPath "$report" --OutputPath "$outputFile" + done + - name: Upload Screenplay HTML report artifact + uses: actions/upload-artifact@v4 + with: + name: Screenplay HTML reports ${{ matrix.browserName }}_${{ matrix.browserVersion }}_${{ matrix.os }}_${{ matrix.osVersion }} + path: Tests/**/ScreenplayReport.html + - name: Fail the build if any test failures + if: steps.dotnet_tests.outcome == 'failure' + run: | + echo "Failing the build due to test failures" + exit 1 diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index 0bfb8788..1695f40f 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -40,6 +40,8 @@ jobs: BranchParam: ${{ github.event_name == 'pull_request' && 'sonar.pullrequest.branch' || 'sonar.branch.name' }} PullRequestParam: ${{ github.event_name == 'pull_request' && format('/d:sonar.pullrequest.key={0}', github.event.number) || '' }} DISPLAY: :99 + # Change selected factory to VerboseChrome to debug Chrome-related issues + WebDriverFactory__SelectedConfiguration: DefaultChrome steps: - name: Checkout @@ -171,7 +173,7 @@ jobs: do reportDir=$(dirname "$report") outputFile="$reportDir/ScreenplayReport.html" - dotnet run --no-build --framework $Tfm --project CSF.Screenplay.JsonToHtmlReport --ReportPath "$report" --OutputPath "$outputFile" + dotnet run --no-build --framework $Tfm -c ${{ env.Configuration }} --project CSF.Screenplay.JsonToHtmlReport --ReportPath "$report" --OutputPath "$outputFile" done - name: Upload Screenplay HTML report artifact uses: actions/upload-artifact@v4 @@ -202,6 +204,3 @@ jobs: with: name: Docs website path: docs/**/* - - # runBrowserTests: - # TODO: Use build-results artifacts and run tests on matrix of browsers diff --git a/CSF.Screenplay.JsonToHtmlReport.Template/src/js/ReportWriter/ReportableElementCreator.js b/CSF.Screenplay.JsonToHtmlReport.Template/src/js/ReportWriter/ReportableElementCreator.js index efc18215..b330a690 100644 --- a/CSF.Screenplay.JsonToHtmlReport.Template/src/js/ReportWriter/ReportableElementCreator.js +++ b/CSF.Screenplay.JsonToHtmlReport.Template/src/js/ReportWriter/ReportableElementCreator.js @@ -87,8 +87,11 @@ export class ReportableElementCreator { } const reportElement = reportableElement.querySelector('.report'); - reportElement.setAttribute('title', 'Performable report; click to expand/collapse'); - reportElement.addEventListener('click', ev => ev.currentTarget.parentElement.classList.toggle('collapsed')); + if(reportElement) { + reportElement.setAttribute('title', 'Performable report; click to expand/collapse'); + reportElement.addEventListener('click', ev => ev.currentTarget.parentElement.classList.toggle('collapsed')); + } + for (const containedReportable of reportable.Reportables) { const reportableElement = this.createReportableElement(containedReportable); diff --git a/CSF.Screenplay.Selenium/BrowseTheWeb.cs b/CSF.Screenplay.Selenium/BrowseTheWeb.cs index 4f88e6ad..ca229ade 100644 --- a/CSF.Screenplay.Selenium/BrowseTheWeb.cs +++ b/CSF.Screenplay.Selenium/BrowseTheWeb.cs @@ -83,6 +83,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { + webDriverAndOptions?.WebDriver.Quit(); webDriverAndOptions?.Dispose(); } disposedValue = true; diff --git a/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs b/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs new file mode 100644 index 00000000..95c63371 --- /dev/null +++ b/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs @@ -0,0 +1,80 @@ +using System; +using System.Globalization; +using CSF.Screenplay.Performables; +using CSF.Screenplay.Selenium.Actions; +using CSF.Screenplay.Selenium.Elements; + +namespace CSF.Screenplay.Selenium.Builders +{ + /// + /// A builder type which creates an instance of . + /// + public class EnterTheDateBuilder : IGetsPerformable + { + readonly DateTime? date; + ITarget target; + CultureInfo culture; + + /// + /// Specifies the target element into which to enter the date. This must be an <input type="date"> element. + /// + /// The target element + /// This same builder, so calls may be chained + /// If is null + /// If this method is used more than once + public EnterTheDateBuilder Into(ITarget target) + { + if (target is null) + throw new ArgumentNullException(nameof(target)); + if(this.target != null) + throw new InvalidOperationException("The target has already been set; it may not be set again."); + + this.target = target; + return this; + } + + /// + /// Specifies the culture for which to enter the date. This must be the culture in which the web browser is operating. + /// + /// + /// + /// Web browser are culture-aware applications and they will render the input/display value of a date field using the culture + /// in which their operating system is configured. This impacts the manner in which users input dates. + /// If this method is not used, the task returned by this builder will use the culture of the operating system/environment + /// that is executing the Screenplay Performance. This is usually OK when running the web browser locally, but it might not match + /// the browser's culture when using remote web browsers. + /// + /// + /// + /// + /// For example, a British English browser en-GB expects dates to be entered in the format ddMMyyyy. + /// However, a US English browser en-US expects dates to be entered in the format MMddyyyy. + /// + /// + /// The parameter of this method must be the culture identifier of the culture which the + /// browser is operating under, such as en-GB. + /// + /// + /// A culture identifier string + /// This same builder, so calls may be chained + /// If is null + /// If indicates a culture which is not found + public EnterTheDateBuilder ForTheCultureNamed(string cultureIdentifier) + { + culture = CultureInfo.GetCultureInfo(cultureIdentifier); + return this; + } + + /// + public IPerformable GetPerformable() => new EnterTheDate(date, target, culture); + + /// + /// Initializes a new instance of the class with the specified date. + /// + /// The date to enter, or null + public EnterTheDateBuilder(DateTime? date) + { + this.date = date; + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj b/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj index c649c3c8..743e2d55 100644 --- a/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj +++ b/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj @@ -15,7 +15,7 @@ - + diff --git a/CSF.Screenplay.Selenium/PerformableBuilder.elementPerformables.cs b/CSF.Screenplay.Selenium/PerformableBuilder.elementPerformables.cs index b3c253ff..add96cb2 100644 --- a/CSF.Screenplay.Selenium/PerformableBuilder.elementPerformables.cs +++ b/CSF.Screenplay.Selenium/PerformableBuilder.elementPerformables.cs @@ -1,3 +1,4 @@ +using System; using CSF.Screenplay.Selenium.Actions; using CSF.Screenplay.Selenium.Builders; using CSF.Screenplay.Selenium.Elements; @@ -32,6 +33,18 @@ public static partial class PerformableBuilder /// A builder with which the user may select a target element. public static SendKeysBuilder EnterTheText(params string[] text) => new SendKeysBuilder(string.Join(string.Empty, text)); + /// + /// Gets a builder for creating a performable action which represents an actor entering a date into an <input type="date"> element. + /// + /// + /// + /// If the specified is then the input element will be cleared. + /// + /// + /// The date to enter into the input control. + /// A builder with which the user may select a target element and optionally a culture. + public static EnterTheDateBuilder EnterTheDate(DateTime? date) => new EnterTheDateBuilder(date); + /// /// Gets a performable which represents an actor deselecting everything from a <select> element. /// diff --git a/CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs b/CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs new file mode 100644 index 00000000..f16af502 --- /dev/null +++ b/CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs @@ -0,0 +1,79 @@ +using System; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions +{ + /// + /// A which represents an actor entering a date value into an <input type="date"> element. + /// + /// + /// + /// Note that this task is culture-sensitive. Ensure that the date value is entered into the browser using the culture in which the browser is + /// running. + /// If no culture information is specified then this task defaults to the current culture: . + /// However, this is not certain to be correct, particularly in remote/cloud configurations where the web browser is operating on different + /// infrastructure to the computer which is executing the Screenplay performance. These two computers might be operating in different cultures. + /// + /// + /// If the date specified to this task is then this task will clear the date from the target. + /// + /// + /// + /// + /// For example, a British English browser en-GB expects dates to be entered in the format ddMMyyyy. + /// However, a US English browser en-US expects dates to be entered in the format MMddyyyy. + /// + /// + public class EnterTheDate : IPerformable, ICanReport + { + const string nonNumericPattern = @"\D"; + + static readonly Regex nonNumeric = new Regex(nonNumericPattern, + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, + TimeSpan.FromMilliseconds(100)); + + readonly DateTime? date; + readonly ITarget target; + readonly CultureInfo culture; + + string GetShortDatePattern() => culture.DateTimeFormat.ShortDatePattern; + + string FormatDate() => date.HasValue ? date.Value.ToString(GetShortDatePattern()) : null; + + /// + public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + { + if(!date.HasValue) + return actor.PerformAsync(ClearTheContentsOf(target), cancellationToken); + + var dateText = nonNumeric.Replace(FormatDate(), string.Empty); + return actor.PerformAsync(EnterTheText(dateText).Into(target), cancellationToken); + } + + /// + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) + { + return date.HasValue + ? formatter.Format("{Actor} enters the date {Date} into {Target}", actor.Name, FormatDate(), target) + : formatter.Format("{Actor} clears the date from {Target}", actor.Name, date, target); + } + + /// + /// Initializes a new instance of the class with the specified date. + /// + /// The date to enter into the element. + /// The element into which to enter the data + /// The culture for which to enter the date + public EnterTheDate(DateTime? date, ITarget target, CultureInfo culture = null) + { + this.date = date; + this.target = target ?? throw new ArgumentNullException(nameof(target)); + this.culture = culture ?? CultureInfo.CurrentCulture; + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay/ScreenplayExtensions.cs b/CSF.Screenplay/ScreenplayExtensions.cs index 8aed7762..41097a2f 100644 --- a/CSF.Screenplay/ScreenplayExtensions.cs +++ b/CSF.Screenplay/ScreenplayExtensions.cs @@ -208,6 +208,19 @@ public static ScopeAndPerformance CreateScopedPerformance(this Screenplay screen return new ScopeAndPerformance(performance, scope); } + /// + /// Gets the event bus from the screenplay's service provider. + /// + /// The screenplay from which to retrieve the event bus. + /// The event bus instance. + /// If is . + public static IHasPerformanceEvents GetEventBus(this Screenplay screenplay) + { + if (screenplay is null) + throw new ArgumentNullException(nameof(screenplay)); + return screenplay.ServiceProvider.GetRequiredService(); + } + static AsyncPerformanceLogic GetAsyncPerformanceLogic(SyncPerformanceLogic syncPerformanceLogic) { return (services, token) => diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/InputDateTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/InputDateTests.html new file mode 100644 index 00000000..af2f5c0a --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/InputDateTests.html @@ -0,0 +1,21 @@ + + + + +

Date input Tests

+

+ Typing into the input box will immediately update the value of the display area, below. +

+ +
+ + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearCookiesTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearCookiesTests.cs index 282240e4..2d08deef 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearCookiesTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearCookiesTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class ClearCookiesTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs index 0ea3f400..f883dd5e 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs @@ -11,7 +11,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class ClearLocalStorageTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearTheContentsTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearTheContentsTests.cs index c7ae7092..2c6ebb9b 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearTheContentsTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearTheContentsTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class ClearTheContentsTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClickTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClickTests.cs index 4c3f2aab..973e32a1 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClickTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClickTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class ClickTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeleteTheCookieTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeleteTheCookieTests.cs index e98bc464..9d345472 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeleteTheCookieTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeleteTheCookieTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class DeleteTheCookieTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectAllTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectAllTests.cs index 347b0954..520abeb7 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectAllTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectAllTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class DeselectAllTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByIndexTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByIndexTests.cs index c395de28..ef76073b 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByIndexTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByIndexTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class DeselectByIndexTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByTextTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByTextTests.cs index 9e136906..fcf50825 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByTextTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByTextTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class DeselectByTextTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByValueTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByValueTests.cs index 01392935..a1fb7570 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByValueTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByValueTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class DeselectByValueTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ExecuteJavaScriptTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ExecuteJavaScriptTests.cs index d3e3103e..c7bd2617 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ExecuteJavaScriptTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ExecuteJavaScriptTests.cs @@ -4,7 +4,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class ExecuteJavaScriptTests { const string scriptBody = """ diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/OpenUrlTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/OpenUrlTests.cs index 332d5e71..0191cddc 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/OpenUrlTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/OpenUrlTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class OpenUrlTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs index 20261bff..8f4e005f 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs @@ -6,7 +6,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class SelectByIndexTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs index 459bd6a5..3ed84074 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs @@ -6,7 +6,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class SelectByTextTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs index fe7dbd96..098a262c 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs @@ -6,7 +6,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class SelectByValueTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SendKeysTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SendKeysTests.cs index 13ce74c0..7e86e419 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SendKeysTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SendKeysTests.cs @@ -4,7 +4,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class SendKeysTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/WaitTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/WaitTests.cs index 2c407228..db9b43b6 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/WaitTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/WaitTests.cs @@ -2,13 +2,12 @@ using System; using CSF.Screenplay.Performables; using CSF.Screenplay.Selenium.Elements; -using OpenQA.Selenium; using static CSF.Screenplay.PerformanceStarter; using static CSF.Screenplay.Selenium.PerformableBuilder; namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class WaitTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackDriverFactory.cs b/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackDriverFactory.cs new file mode 100644 index 00000000..a03a7ad7 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackDriverFactory.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using CSF.Extensions.WebDriver.Factories; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Edge; +using OpenQA.Selenium.Firefox; +using OpenQA.Selenium.Remote; +using OpenQA.Selenium.Safari; +using static CSF.Screenplay.Selenium.BrowserStack.BrowserStackEnvironment; + +namespace CSF.Screenplay.Selenium.BrowserStack; + +/// +/// Implementation of which creates drivers for BrowserStack. +/// +/// +/// +/// I'm using this instead of the BrowserStack SDK because I'm already modifying the way that tests run via Screenplay, so I want to avoid +/// messing with them twice. Later, I can give a try with the official SDK to see if its compatible with Screenplay. +/// +/// +public class BrowserStackDriverFactory : ICreatesWebDriverFromOptions +{ + const string AdditionalOptionsCapabilityName = "bstack:options"; + + const string GridUrl = "https://hub-cloud.browserstack.com/wd/hub/"; + + public WebDriverAndOptions GetWebDriver(WebDriverCreationOptions options, Action? supplementaryConfiguration = null) + { + var driverOptions = GetDriverOptions(); + driverOptions.AddAdditionalOption(AdditionalOptionsCapabilityName, GetBrowserStackOptions()); + var driver = new RemoteWebDriver(new Uri(GridUrl), driverOptions); + return new (driver, driverOptions); + } + + static DriverOptions GetDriverOptions() + { + var browserName = GetBrowserName(); + return browserName switch + { + "Chrome" => new ChromeOptions(), + "Edge" => new EdgeOptions(), + "Firefox" => new FirefoxOptions(), + "Safari" => new SafariOptions(), + _ => throw new InvalidOperationException($"The {BrowserName} environment variable: '{GetBrowserName()}' must indicate a supported browser"), + }; + } + + static Dictionary GetBrowserStackOptions() + { + return new () + { + { "os", GetOperatingSystem() }, + { "osVersion", GetOperatingSystemVersion() }, + { "browserVersion", GetBrowserVersion() }, + { "userName", GetBrowserStackUserName() }, + { "accessKey", GetBrowserStackAccessKey() }, + { "local", bool.TrueString.ToLowerInvariant() }, + { "projectName", GetProjectName() }, + { "buildName", GetBuildName() }, + { "sessionName", GetTestName() } + }; + } + + static string GetTestName() => TestContext.CurrentContext.Test.FullName; +} diff --git a/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackEnvironment.cs b/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackEnvironment.cs new file mode 100644 index 00000000..e74637ba --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackEnvironment.cs @@ -0,0 +1,36 @@ +using System; +using static System.Environment; + +namespace CSF.Screenplay.Selenium.BrowserStack; + +/// +/// Static helper provides access to environment variables which control . +/// +public static class BrowserStackEnvironment +{ + internal const string + BrowserName = "BSbrowserName", + BrowserVersion = "BSbrowserVersion", + OperatingSystem = "BSos", + OperatingSystemVersion = "BSosVersion", + BrowserStackUserName = "BSuserName", + BrowserStackAccessKey = "BSaccessKey", + ProjectName = "BSprojectName", + BuildName = "BSbuildName", + SelectedWebDriverConfig = "WebDriverFactory__SelectedConfiguration", + BrowserStackConfigName = "BrowserStack"; + + internal static string? GetBrowserName() => GetEnvironmentVariable(BrowserName); + internal static string? GetBrowserVersion() => GetEnvironmentVariable(BrowserVersion); + internal static string? GetOperatingSystem() => GetEnvironmentVariable(OperatingSystem); + internal static string? GetOperatingSystemVersion() => GetEnvironmentVariable(OperatingSystemVersion); + internal static string? GetBrowserStackUserName() => GetEnvironmentVariable(BrowserStackUserName); + internal static string? GetBrowserStackAccessKey() => GetEnvironmentVariable(BrowserStackAccessKey); + internal static string? GetProjectName() => GetEnvironmentVariable(ProjectName); + internal static string? GetBuildName() => GetEnvironmentVariable(BuildName); + + /// + /// Gets a value indicating whether the current test run is running on BrowserStack + /// + internal static bool IsBrowserStackTest() => GetEnvironmentVariable(SelectedWebDriverConfig) == BrowserStackConfigName; +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackExtension.cs b/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackExtension.cs new file mode 100644 index 00000000..1eae5c10 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackExtension.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Reflection; +using System.Text; +using BrowserStack; +using CSF.Screenplay.Performances; + +namespace CSF.Screenplay.Selenium.BrowserStack; + +public sealed class BrowserStackExtension : IDisposable +{ + const string urlPattern = "https://www.browserstack.com/automate/sessions/{0}.json"; + + Local? browserStackLocal; + IHasPerformanceEvents? eventBus; + HttpClient? httpClient; + + public async Task OnTestRunStarting() + { + if(!BrowserStackEnvironment.IsBrowserStackTest()) return; + + browserStackLocal = new Local(); + browserStackLocal.start(GetBrowserStackLocalArgs().ToList()); + for(var i = 0; i < 80; i++) + { + Console.WriteLine("Waiting for BrowserStackLocal to start up, attempt {0} of {1} ...", i + 1, 80); + await Task.Delay(250); + if(browserStackLocal.isRunning()) break; + } + if(!browserStackLocal.isRunning()) throw new TimeoutException("BrowserStack Local is still not running after 20 seconds"); + + httpClient = GetHttpClient(); + + eventBus = ScreenplayLocator.GetScreenplay(Assembly.GetExecutingAssembly()).GetEventBus(); + eventBus.PerformanceFinished += UpdateBrowserStackSession; + + } + + void UpdateBrowserStackSession(object? sender, PerformanceFinishedEventArgs e) + { + if(!e.Success.HasValue) return; + + var sessionId = BrowserStackSessionIdProvider.GetBrowserStackSessionId(e.Performance); + if(sessionId is null) return; + + var uri = GetApiUri(sessionId); + var requestMessage = GetRequestMessage(uri, e.Success.Value); + httpClient!.Send(requestMessage); + } + + static HttpRequestMessage GetRequestMessage(Uri requestUri, bool success) + { + return new (HttpMethod.Put, requestUri) + { + Content = JsonContent.Create(@$"{{""status"":""{ (success ? "passed" : "failed") }"", ""reason"":""""}}"), + }; + } + + static Uri GetApiUri(string sessionId) => new(string.Format(urlPattern, sessionId)); + + static HttpClient GetHttpClient() + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = GetAuthenticationHeaderValue(); + return client; + } + + static AuthenticationHeaderValue GetAuthenticationHeaderValue() + { + var headerBytes = Encoding.ASCII.GetBytes($"{BrowserStackEnvironment.GetBrowserStackUserName()}:{BrowserStackEnvironment.GetBrowserStackAccessKey()}"); + var headerValue = Convert.ToBase64String(headerBytes); + return new ("Basic", headerValue); + } + + /// + public void Dispose() + { + browserStackLocal?.stop(); + httpClient?.Dispose(); + + if(eventBus is null) return; + eventBus.PerformanceFinished -= UpdateBrowserStackSession; + } + + static Dictionary GetBrowserStackLocalArgs() + { + return new () + { + { "key", BrowserStackEnvironment.GetBrowserStackAccessKey()! }, + }; + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackSessionIdProvider.cs b/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackSessionIdProvider.cs new file mode 100644 index 00000000..23a134c3 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/BrowserStack/BrowserStackSessionIdProvider.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using System.Text.Json.Nodes; + +namespace CSF.Screenplay.Selenium.BrowserStack; + +public static class BrowserStackSessionIdProvider +{ + public static string? GetBrowserStackSessionId(IPerformance performance) + { + var performanceCast = performance.ServiceProvider.GetRequiredService(); + var actors = performanceCast.GetCastList().Select(name => performanceCast.GetActor(name)); + var browseTheWeb = actors.FirstOrDefault((ICanPerform actor) => actor.HasAbility())?.GetAbility(); + if(browseTheWeb is null) return null; + + var javascriptExecutor = browseTheWeb.GetJavaScriptExecutor(); + var sessionDetailsJson = (string) javascriptExecutor.ExecuteScript("browserstack_executor: {\"action\": \"getSessionDetails\"}"); + var sessionDetails = JsonNode.Parse(sessionDetailsJson)!; + return (string?) sessionDetails["hashed_id"]; + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Builders/QueryPredicatePrototypeBuilderTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Builders/QueryPredicatePrototypeBuilderTests.cs index 85d491ab..637515a4 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Builders/QueryPredicatePrototypeBuilderTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Builders/QueryPredicatePrototypeBuilderTests.cs @@ -7,7 +7,7 @@ namespace CSF.Screenplay.Selenium.Builders; -[TestFixture] +[TestFixture, Parallelizable] public class QueryPredicatePrototypeBuilderTests { [Test, AutoMoqData] diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Builders/UnnamedWaitBuilderTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Builders/UnnamedWaitBuilderTests.cs index 32857132..2c48b1b0 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Builders/UnnamedWaitBuilderTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Builders/UnnamedWaitBuilderTests.cs @@ -1,5 +1,4 @@ using System; -using System.Reflection; using CSF.Screenplay.Performables; using CSF.Screenplay.Selenium.Actions; diff --git a/Tests/CSF.Screenplay.Selenium.Tests/CSF.Screenplay.Selenium.Tests.csproj b/Tests/CSF.Screenplay.Selenium.Tests/CSF.Screenplay.Selenium.Tests.csproj index 00908a6b..abe377c4 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/CSF.Screenplay.Selenium.Tests.csproj +++ b/Tests/CSF.Screenplay.Selenium.Tests/CSF.Screenplay.Selenium.Tests.csproj @@ -19,9 +19,12 @@ + + + diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Elements/FilterElementsTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Elements/FilterElementsTests.cs index 823cc11f..1bd0d1ab 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Elements/FilterElementsTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Elements/FilterElementsTests.cs @@ -1,10 +1,9 @@ namespace CSF.Screenplay.Selenium.Elements; -using CSF.Specifications; using static CSF.Screenplay.PerformanceStarter; using static CSF.Screenplay.Selenium.PerformableBuilder; -[TestFixture] +[TestFixture, Parallelizable] public class FilterElementsTests { static readonly Locator diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Elements/LocatorTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Elements/LocatorTests.cs index 51cfc2f9..d6497e5f 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Elements/LocatorTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Elements/LocatorTests.cs @@ -2,7 +2,7 @@ namespace CSF.Screenplay.Selenium.Elements; using static CSF.Screenplay.PerformanceStarter; using static CSF.Screenplay.Selenium.PerformableBuilder; -[TestFixture, Description("Tests for various subclasses of Locator")] +[TestFixture, Description("Tests for various subclasses of Locator"), Parallelizable] public class LocatorTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Queries/QueriesTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Queries/QueriesTests.cs index 6f300c5c..c492a792 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Queries/QueriesTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Queries/QueriesTests.cs @@ -6,7 +6,7 @@ namespace CSF.Screenplay.Selenium.Queries; using static CSF.Screenplay.PerformanceStarter; using static CSF.Screenplay.Selenium.PerformableBuilder; -[TestFixture, Description("Tests for many classes in the Queries namespace")] +[TestFixture, Description("Tests for many classes in the Queries namespace"), Parallelizable] public class QueriesTests { static readonly ITarget diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Questions/ExecuteJavaScriptAndGetResultTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Questions/ExecuteJavaScriptAndGetResultTests.cs index 914e230a..af9e36b1 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Questions/ExecuteJavaScriptAndGetResultTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Questions/ExecuteJavaScriptAndGetResultTests.cs @@ -4,7 +4,7 @@ namespace CSF.Screenplay.Selenium.Actions; -[TestFixture] +[TestFixture, Parallelizable] public class ExecuteJavaScriptAndGetResultTests { const string scriptBody1 = """ diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Questions/FindElementsTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Questions/FindElementsTests.cs index 028cef8a..d0314f6c 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Questions/FindElementsTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Questions/FindElementsTests.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Questions; -[TestFixture] +[TestFixture, Parallelizable] public class FindElementsTests { static readonly Locator diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetWindowTitleTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetWindowTitleTests.cs index 374f08b9..675d5116 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetWindowTitleTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetWindowTitleTests.cs @@ -3,7 +3,7 @@ namespace CSF.Screenplay.Selenium.Questions; -[TestFixture] +[TestFixture, Parallelizable] public class GetWindowTitleTests { static readonly NamedUri testPage = new NamedUri("OpenUrlTests.html", "the test page"); diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Questions/TakeScreenshotTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Questions/TakeScreenshotTests.cs index 9a9113ff..45ea41f5 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Questions/TakeScreenshotTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Questions/TakeScreenshotTests.cs @@ -3,7 +3,7 @@ namespace CSF.Screenplay.Selenium.Questions; -[TestFixture] +[TestFixture, Parallelizable] public class TakeScreenshotTests { static readonly NamedUri testPage = new NamedUri("OpenUrlTests.html", "the test page"); diff --git a/Tests/CSF.Screenplay.Selenium.Tests/ScreenplayFactory.cs b/Tests/CSF.Screenplay.Selenium.Tests/ScreenplayFactory.cs index 113e6b73..5fde4d84 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/ScreenplayFactory.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/ScreenplayFactory.cs @@ -1,6 +1,7 @@ using CSF.Extensions.WebDriver; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace CSF.Screenplay.Selenium; @@ -12,6 +13,7 @@ public Screenplay GetScreenplay() { services.AddSingleton(GetConfiguration()); services.AddWebDriverFactory(); + services.AddLogging(l => l.AddConsole(c => c.LogToStandardErrorThreshold = LogLevel.Warning)); services.AddTransient(); services.AddTransient(); diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/ClickAndWaitForDocumentReadyTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/ClickAndWaitForDocumentReadyTests.cs index 7f55d7fe..907afe03 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/ClickAndWaitForDocumentReadyTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/ClickAndWaitForDocumentReadyTests.cs @@ -7,7 +7,7 @@ namespace CSF.Screenplay.Selenium.Tasks; -[TestFixture] +[TestFixture, Parallelizable] public class ClickAndWaitForDocumentReadyTests { static readonly NamedUri startPage = new NamedUri("DelayedNavigation.html", "the test page"); diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/EnterTheDateTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/EnterTheDateTests.cs new file mode 100644 index 00000000..08cbb1fb --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/EnterTheDateTests.cs @@ -0,0 +1,60 @@ +using System; +using System.Globalization; +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Tasks; + +[TestFixture, Parallelizable] +public class EnterTheDateTests +{ + static readonly ITarget + inputArea = new ElementId("inputArea", "the input area"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("InputDateTests.html", "the test page"); + + [Test, Screenplay] + public async Task EnteringADateShouldYieldTheCorrectValue(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(EnterTheDate(new DateTime(2025, 11, 12)).Into(inputArea)); + var result = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(result, Is.EqualTo("2025-11-12")); + } + + [Test, Screenplay] + public async Task EnteringANullDateShouldClearTheValue(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(EnterTheDate(null).Into(inputArea)); + var result = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test, Screenplay] + public async Task EnteringADateInAnUnusualCultureShouldYieldIncorrectResults(IStage stage) + { + var webster = stage.Spotlight(); + + if(CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern.StartsWith("y", StringComparison.InvariantCultureIgnoreCase)) + Assert.Inconclusive("This test can't be meaningfully run when the current culture uses Y/M/D date formatting"); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(EnterTheDate(new DateTime(2025, 11, 12)).Into(inputArea).ForTheCultureNamed("ja-JP")); + var result = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.Multiple(() => + { + Assert.That(result, Is.Not.EqualTo(string.Empty), "The date shouldn't be empty"); + Assert.That(result, Is.Not.EqualTo("2025-11-12"), "The date shouldn't be the value which was entered either, because of the culture/format difference"); + }); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/TakeAndSaveAScreenshotTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/TakeAndSaveAScreenshotTests.cs index 05a93930..d73ab8b6 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/TakeAndSaveAScreenshotTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/TakeAndSaveAScreenshotTests.cs @@ -4,7 +4,7 @@ namespace CSF.Screenplay.Selenium.Questions; -[TestFixture] +[TestFixture, Parallelizable] public class TakeAndSaveAScreenshotTests { static readonly NamedUri testPage = new NamedUri("OpenUrlTests.html", "the test page"); diff --git a/Tests/CSF.Screenplay.Selenium.Tests/TestWebappSetupAndTeardown.cs b/Tests/CSF.Screenplay.Selenium.Tests/TestWebappSetupAndTeardown.cs index 7598be40..791f4249 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/TestWebappSetupAndTeardown.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/TestWebappSetupAndTeardown.cs @@ -2,6 +2,8 @@ using System.Diagnostics; using System.IO; using System.Net.Http; +using CSF.Screenplay.Performances; +using CSF.Screenplay.Selenium.BrowserStack; namespace CSF.Screenplay.Selenium; @@ -12,12 +14,16 @@ public class TestWebappSetupAndTeardown const int secondsDelay = 2; static Process? webAppProcess; + static BrowserStackExtension browserStack; [OneTimeSetUp] public async Task StartWebAppAsync() { webAppProcess = Process.Start("dotnet", $"run --project {GetPathToWebappProject()}"); await WaitForWebAppToBeAvailableAsync(); + + browserStack = new BrowserStackExtension(); + await browserStack.OnTestRunStarting(); } [OneTimeTearDown] @@ -25,6 +31,8 @@ public void StopWebApp() { webAppProcess?.Kill(true); webAppProcess?.Dispose(); + + browserStack?.Dispose(); } /// diff --git a/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json b/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json index 2e931337..05665cd4 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json +++ b/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json @@ -7,6 +7,10 @@ "VerboseChrome": { "DriverType": "ChromeDriver", "DriverFactoryType": "CSF.Screenplay.Selenium.VerboseChromeDriverFactory, CSF.Screenplay.Selenium.Tests" + }, + "BrowserStack": { + "DriverType": "RemoteWebDriver", + "DriverFactoryType": "CSF.Screenplay.Selenium.BrowserStack.BrowserStackDriverFactory, CSF.Screenplay.Selenium.Tests" } }, "SelectedConfiguration": "DefaultChrome" diff --git a/Tests/CSF.Screenplay.Tests/ScreenplayExtensionsTests.cs b/Tests/CSF.Screenplay.Tests/ScreenplayExtensionsTests.cs index 3e2883ea..c661674b 100644 --- a/Tests/CSF.Screenplay.Tests/ScreenplayExtensionsTests.cs +++ b/Tests/CSF.Screenplay.Tests/ScreenplayExtensionsTests.cs @@ -1,5 +1,6 @@ using CSF.Screenplay.Performances; using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework.Internal; namespace CSF.Screenplay; @@ -71,6 +72,18 @@ public void ExecuteAsPerformanceGenericShouldExecuteThePerformanceHostLogic() Assert.That(sut.ServiceProvider.GetRequiredService().HasExecuted, Is.True); } + [Test, AutoMoqData] + public void GetEventBusShouldReturnAnEventBus([DefaultScreenplay] Screenplay sut) + { + Assert.That(() => sut.GetEventBus(), Is.InstanceOf()); + } + + [Test, AutoMoqData] + public void GetEventBusShouldThrowIfScreenplayIsNull() + { + Assert.That(() => ((Screenplay?) null).GetEventBus(), Throws.ArgumentNullException); + } + public class SamplePerformanceHost : IHostsPerformance { public bool HasExecuted { get; set; }