Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions .github/workflows/crossBrowserTesting.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 3 additions & 4 deletions .github/workflows/dotnetCi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -202,6 +204,3 @@ jobs:
with:
name: Docs website
path: docs/**/*

# runBrowserTests:
# TODO: Use build-results artifacts and run tests on matrix of browsers
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions CSF.Screenplay.Selenium/BrowseTheWeb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ protected virtual void Dispose(bool disposing)
{
if (disposing)
{
webDriverAndOptions?.WebDriver.Quit();
webDriverAndOptions?.Dispose();
}
disposedValue = true;
Expand Down
80 changes: 80 additions & 0 deletions CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A builder type which creates an instance of <see cref="EnterTheDate"/>.
/// </summary>
public class EnterTheDateBuilder : IGetsPerformable
{
readonly DateTime? date;
ITarget target;
CultureInfo culture;

/// <summary>
/// Specifies the target element into which to enter the date. This must be an <c>&lt;input type="date"&gt;</c> element.
/// </summary>
/// <param name="target">The target element</param>
/// <returns>This same builder, so calls may be chained</returns>
/// <exception cref="ArgumentNullException">If <paramref name="target"/> is null</exception>
/// <exception cref="InvalidOperationException">If this method is used more than once</exception>
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;
}

/// <summary>
/// Specifies the culture for which to enter the date. This must be the culture in which the web browser is operating.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <example>
/// <para>
/// For example, a British English browser <c>en-GB</c> expects dates to be entered in the format ddMMyyyy.
/// However, a US English browser <c>en-US</c> expects dates to be entered in the format MMddyyyy.
/// </para>
/// <para>
/// The <paramref name="cultureIdentifier"/> parameter of this method must be the culture identifier of the culture which the
/// browser is operating under, such as <c>en-GB</c>.
/// </para>
/// </example>
/// <param name="cultureIdentifier">A culture identifier string</param>
/// <returns>This same builder, so calls may be chained</returns>
/// <exception cref="ArgumentNullException">If <paramref name="cultureIdentifier"/> is null</exception>
/// <exception cref="CultureNotFoundException">If <paramref name="cultureIdentifier"/> indicates a culture which is not found</exception>
public EnterTheDateBuilder ForTheCultureNamed(string cultureIdentifier)
{
culture = CultureInfo.GetCultureInfo(cultureIdentifier);
return this;
}

/// <inheritdoc/>
public IPerformable GetPerformable() => new EnterTheDate(date, target, culture);

/// <summary>
/// Initializes a new instance of the <see cref="EnterTheDateBuilder"/> class with the specified date.
/// </summary>
/// <param name="date">The date to enter, or null</param>
public EnterTheDateBuilder(DateTime? date)
{
this.date = date;
}
}
}
2 changes: 1 addition & 1 deletion CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == '$(DotNetFrameworkLegacy)'" />
<PackageReference Include="Selenium.WebDriver" Version="4.0.0" />
<PackageReference Include="Selenium.Support" Version="4.0.0" />
<PackageReference Include="CSF.Extensions.WebDriver" Version="2.0.0-3.beta" />
<PackageReference Include="CSF.Extensions.WebDriver" Version="2.0.0-4.beta" />
<PackageReference Include="CSF.Specifications" Version="2.0.0" />
</ItemGroup>

Expand Down
13 changes: 13 additions & 0 deletions CSF.Screenplay.Selenium/PerformableBuilder.elementPerformables.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using CSF.Screenplay.Selenium.Actions;
using CSF.Screenplay.Selenium.Builders;
using CSF.Screenplay.Selenium.Elements;
Expand Down Expand Up @@ -32,6 +33,18 @@ public static partial class PerformableBuilder
/// <returns>A builder with which the user may select a target element.</returns>
public static SendKeysBuilder EnterTheText(params string[] text) => new SendKeysBuilder(string.Join(string.Empty, text));

/// <summary>
/// Gets a builder for creating a performable action which represents an actor entering a date into an <c>&lt;input type="date"&gt;</c> element.
/// </summary>
/// <remarks>
/// <para>
/// If the specified <paramref name="date"/> is <see langword="null"/> then the input element will be cleared.
/// </para>
/// </remarks>
/// <param name="date">The date to enter into the input control.</param>
/// <returns>A builder with which the user may select a target element and optionally a culture.</returns>
public static EnterTheDateBuilder EnterTheDate(DateTime? date) => new EnterTheDateBuilder(date);

/// <summary>
/// Gets a performable which represents an actor deselecting everything from a <c>&lt;select&gt;</c> element.
/// </summary>
Expand Down
79 changes: 79 additions & 0 deletions CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A <see cref="IPerformable"/> which represents an actor entering a date value into an <c>&lt;input type="date"&gt;</c> element.
/// </summary>
/// <remarks>
/// <para>
/// 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: <see cref="CultureInfo.CurrentCulture"/>.
/// 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.
/// </para>
/// <para>
/// If the date specified to this task is <see langword="null"/> then this task will clear the date from the target.
/// </para>
/// </remarks>
/// <example>
/// <para>
/// For example, a British English browser <c>en-GB</c> expects dates to be entered in the format ddMMyyyy.
/// However, a US English browser <c>en-US</c> expects dates to be entered in the format MMddyyyy.
/// </para>
/// </example>
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;

/// <inheritdoc/>
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);
}

/// <inheritdoc/>
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);
}

/// <summary>
/// Initializes a new instance of the <see cref="EnterTheDate"/> class with the specified date.
/// </summary>
/// <param name="date">The date to enter into the element.</param>
/// <param name="target">The element into which to enter the data</param>
/// <param name="culture">The culture for which to enter the date</param>
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;
}
}
}
13 changes: 13 additions & 0 deletions CSF.Screenplay/ScreenplayExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,19 @@ public static ScopeAndPerformance CreateScopedPerformance(this Screenplay screen
return new ScopeAndPerformance(performance, scope);
}

/// <summary>
/// Gets the event bus from the screenplay's service provider.
/// </summary>
/// <param name="screenplay">The screenplay from which to retrieve the event bus.</param>
/// <returns>The <see cref="IHasPerformanceEvents"/> event bus instance.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="screenplay"/> is <see langword="null" />.</exception>
public static IHasPerformanceEvents GetEventBus(this Screenplay screenplay)
{
if (screenplay is null)
throw new ArgumentNullException(nameof(screenplay));
return screenplay.ServiceProvider.GetRequiredService<IHasPerformanceEvents>();
}

static AsyncPerformanceLogic GetAsyncPerformanceLogic(SyncPerformanceLogic syncPerformanceLogic)
{
return (services, token) =>
Expand Down
Loading
Loading