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; }