Skip to content

Commit a825f59

Browse files
authored
Merge pull request #271 from csf-dev/craigfowler/issue250
Resolves #250 Initial work towards testing with BrowserStack
2 parents 2180c30 + 655fa6b commit a825f59

47 files changed

Lines changed: 686 additions & 36 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: Cross-browser testing
2+
3+
on:
4+
push:
5+
branches: [ "master" ]
6+
pull_request:
7+
branches: [ "master" ]
8+
schedule:
9+
- cron: "27 20 * * 0"
10+
11+
jobs:
12+
13+
# Summary:
14+
#
15+
# * Installs and configures the environment
16+
# * Builds the solution in Debug configuration
17+
# * Runs just the Selenium tests
18+
# * In Debug configuration (.NET tests)
19+
# * Using the BrowserStack browser configuration
20+
21+
browser_tests:
22+
23+
strategy:
24+
max-parallel: 1
25+
fail-fast: false
26+
matrix:
27+
include:
28+
- browserName: Chrome
29+
browserVersion: latest-50
30+
os: Windows
31+
osVersion: 11
32+
- browserName: Chrome
33+
browserVersion: latest
34+
os: Windows
35+
osVersion: 11
36+
- browserName: Edge
37+
browserVersion: latest
38+
os: Windows
39+
osVersion: 11
40+
- browserName: Firefox
41+
browserVersion: latest
42+
os: Windows
43+
osVersion: 11
44+
- browserName: Firefox
45+
browserVersion: latest-40
46+
os: Windows
47+
osVersion: 11
48+
- browserName: Safari
49+
browserVersion: 17.3
50+
os: OS X
51+
osVersion: Sonoma
52+
- browserName: Safari
53+
browserVersion: 26.2
54+
os: OS X
55+
osVersion: Tahoe
56+
57+
name: Run tests
58+
runs-on: ubuntu-24.04
59+
timeout-minutes: 30
60+
61+
env:
62+
RunNumber: ${{ github.run_number }}.${{ github.run_attempt }}
63+
VersionSuffix: crossbrowser.${{ github.run_number }}
64+
Configuration: Release
65+
Tfm: net8.0
66+
DotnetVersion: 8.0.x
67+
WebDriverFactory__SelectedConfiguration: BrowserStack
68+
BSbrowserName: ${{ matrix.browserName }}
69+
BSbrowserVersion: ${{ matrix.browserVersion }}
70+
BSos: ${{ matrix.os }}
71+
BSosVersion: ${{ matrix.osVersion }}
72+
BSuserName: ${{ secrets.BROWSERSTACK_USERNAME }}
73+
BSaccessKey: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
74+
BSprojectName: CSF.Screenplay
75+
BSbuildName: ghActionsRun.${{ github.run_number }}.${{ github.run_attempt }}_${{ matrix.browserName }}:${{ matrix.browserVersion }}_${{ matrix.os }}:${{ matrix.osVersion }}
76+
BSparallelism: 5
77+
78+
steps:
79+
- name: Checkout
80+
uses: actions/checkout@v4
81+
82+
# Install build dependencies
83+
84+
- name: Install .NET
85+
uses: actions/setup-dotnet@v4
86+
with:
87+
dotnet-version: ${{ env.DotnetVersion }}
88+
- name: Install Node.js for building JSON-to-HTML report converter
89+
uses: actions/setup-node@v6.2.0
90+
91+
# Environment setup pre-build
92+
93+
- name: Restore .NET packages
94+
run: dotnet restore
95+
- name: Restore Node modules
96+
run: |
97+
cd CSF.Screenplay.JsonToHtmlReport.Template/src
98+
npm ci
99+
cd ../..
100+
101+
# Build and test the solution
102+
103+
- name: Build the solution
104+
run: dotnet build -c ${{ env.Configuration }}
105+
- name: Run .NET Selenium tests only
106+
id: dotnet_tests
107+
run: dotnet test -c ${{ env.Configuration }} --no-build Tests/CSF.Screenplay.Selenium.Tests -- NumberOfTestWorkers=$BSparallelism
108+
continue-on-error: true
109+
110+
# Post-test tasks (artifacts, overall status)
111+
- name: Upload Screenplay JSON report artifact
112+
uses: actions/upload-artifact@v4
113+
with:
114+
name: Screenplay JSON reports ${{ matrix.browserName }}_${{ matrix.browserVersion }}_${{ matrix.os }}_${{ matrix.osVersion }}
115+
path: Tests/CSF.Screenplay.Selenium.Tests/**/ScreenplayReport_*.json
116+
- name: Convert Screenplay reports to HTML
117+
continue-on-error: true
118+
run: |
119+
for report in $(find Tests/CSF.Screenplay.Selenium.Tests/ -type f -name "ScreenplayReport_*.json")
120+
do
121+
reportDir=$(dirname "$report")
122+
outputFile="$reportDir/ScreenplayReport.html"
123+
dotnet run --no-build --framework $Tfm -c ${{ env.Configuration }} --project CSF.Screenplay.JsonToHtmlReport --ReportPath "$report" --OutputPath "$outputFile"
124+
done
125+
- name: Upload Screenplay HTML report artifact
126+
uses: actions/upload-artifact@v4
127+
with:
128+
name: Screenplay HTML reports ${{ matrix.browserName }}_${{ matrix.browserVersion }}_${{ matrix.os }}_${{ matrix.osVersion }}
129+
path: Tests/**/ScreenplayReport.html
130+
- name: Fail the build if any test failures
131+
if: steps.dotnet_tests.outcome == 'failure'
132+
run: |
133+
echo "Failing the build due to test failures"
134+
exit 1

.github/workflows/dotnetCi.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ jobs:
4040
BranchParam: ${{ github.event_name == 'pull_request' && 'sonar.pullrequest.branch' || 'sonar.branch.name' }}
4141
PullRequestParam: ${{ github.event_name == 'pull_request' && format('/d:sonar.pullrequest.key={0}', github.event.number) || '' }}
4242
DISPLAY: :99
43+
# Change selected factory to VerboseChrome to debug Chrome-related issues
44+
WebDriverFactory__SelectedConfiguration: DefaultChrome
4345

4446
steps:
4547
- name: Checkout
@@ -171,7 +173,7 @@ jobs:
171173
do
172174
reportDir=$(dirname "$report")
173175
outputFile="$reportDir/ScreenplayReport.html"
174-
dotnet run --no-build --framework $Tfm --project CSF.Screenplay.JsonToHtmlReport --ReportPath "$report" --OutputPath "$outputFile"
176+
dotnet run --no-build --framework $Tfm -c ${{ env.Configuration }} --project CSF.Screenplay.JsonToHtmlReport --ReportPath "$report" --OutputPath "$outputFile"
175177
done
176178
- name: Upload Screenplay HTML report artifact
177179
uses: actions/upload-artifact@v4
@@ -202,6 +204,3 @@ jobs:
202204
with:
203205
name: Docs website
204206
path: docs/**/*
205-
206-
# runBrowserTests:
207-
# TODO: Use build-results artifacts and run tests on matrix of browsers

CSF.Screenplay.JsonToHtmlReport.Template/src/js/ReportWriter/ReportableElementCreator.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ export class ReportableElementCreator {
8787
}
8888

8989
const reportElement = reportableElement.querySelector('.report');
90-
reportElement.setAttribute('title', 'Performable report; click to expand/collapse');
91-
reportElement.addEventListener('click', ev => ev.currentTarget.parentElement.classList.toggle('collapsed'));
90+
if(reportElement) {
91+
reportElement.setAttribute('title', 'Performable report; click to expand/collapse');
92+
reportElement.addEventListener('click', ev => ev.currentTarget.parentElement.classList.toggle('collapsed'));
93+
}
94+
9295

9396
for (const containedReportable of reportable.Reportables) {
9497
const reportableElement = this.createReportableElement(containedReportable);

CSF.Screenplay.Selenium/BrowseTheWeb.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ protected virtual void Dispose(bool disposing)
8383
{
8484
if (disposing)
8585
{
86+
webDriverAndOptions?.WebDriver.Quit();
8687
webDriverAndOptions?.Dispose();
8788
}
8889
disposedValue = true;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Globalization;
3+
using CSF.Screenplay.Performables;
4+
using CSF.Screenplay.Selenium.Actions;
5+
using CSF.Screenplay.Selenium.Elements;
6+
7+
namespace CSF.Screenplay.Selenium.Builders
8+
{
9+
/// <summary>
10+
/// A builder type which creates an instance of <see cref="EnterTheDate"/>.
11+
/// </summary>
12+
public class EnterTheDateBuilder : IGetsPerformable
13+
{
14+
readonly DateTime? date;
15+
ITarget target;
16+
CultureInfo culture;
17+
18+
/// <summary>
19+
/// Specifies the target element into which to enter the date. This must be an <c>&lt;input type="date"&gt;</c> element.
20+
/// </summary>
21+
/// <param name="target">The target element</param>
22+
/// <returns>This same builder, so calls may be chained</returns>
23+
/// <exception cref="ArgumentNullException">If <paramref name="target"/> is null</exception>
24+
/// <exception cref="InvalidOperationException">If this method is used more than once</exception>
25+
public EnterTheDateBuilder Into(ITarget target)
26+
{
27+
if (target is null)
28+
throw new ArgumentNullException(nameof(target));
29+
if(this.target != null)
30+
throw new InvalidOperationException("The target has already been set; it may not be set again.");
31+
32+
this.target = target;
33+
return this;
34+
}
35+
36+
/// <summary>
37+
/// Specifies the culture for which to enter the date. This must be the culture in which the web browser is operating.
38+
/// </summary>
39+
/// <remarks>
40+
/// <para>
41+
/// Web browser are culture-aware applications and they will render the input/display value of a date field using the culture
42+
/// in which their operating system is configured. This impacts the manner in which users input dates.
43+
/// If this method is not used, the task returned by this builder will use the culture of the operating system/environment
44+
/// that is executing the Screenplay Performance. This is usually OK when running the web browser locally, but it might not match
45+
/// the browser's culture when using remote web browsers.
46+
/// </para>
47+
/// </remarks>
48+
/// <example>
49+
/// <para>
50+
/// For example, a British English browser <c>en-GB</c> expects dates to be entered in the format ddMMyyyy.
51+
/// However, a US English browser <c>en-US</c> expects dates to be entered in the format MMddyyyy.
52+
/// </para>
53+
/// <para>
54+
/// The <paramref name="cultureIdentifier"/> parameter of this method must be the culture identifier of the culture which the
55+
/// browser is operating under, such as <c>en-GB</c>.
56+
/// </para>
57+
/// </example>
58+
/// <param name="cultureIdentifier">A culture identifier string</param>
59+
/// <returns>This same builder, so calls may be chained</returns>
60+
/// <exception cref="ArgumentNullException">If <paramref name="cultureIdentifier"/> is null</exception>
61+
/// <exception cref="CultureNotFoundException">If <paramref name="cultureIdentifier"/> indicates a culture which is not found</exception>
62+
public EnterTheDateBuilder ForTheCultureNamed(string cultureIdentifier)
63+
{
64+
culture = CultureInfo.GetCultureInfo(cultureIdentifier);
65+
return this;
66+
}
67+
68+
/// <inheritdoc/>
69+
public IPerformable GetPerformable() => new EnterTheDate(date, target, culture);
70+
71+
/// <summary>
72+
/// Initializes a new instance of the <see cref="EnterTheDateBuilder"/> class with the specified date.
73+
/// </summary>
74+
/// <param name="date">The date to enter, or null</param>
75+
public EnterTheDateBuilder(DateTime? date)
76+
{
77+
this.date = date;
78+
}
79+
}
80+
}

CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == '$(DotNetFrameworkLegacy)'" />
1616
<PackageReference Include="Selenium.WebDriver" Version="4.0.0" />
1717
<PackageReference Include="Selenium.Support" Version="4.0.0" />
18-
<PackageReference Include="CSF.Extensions.WebDriver" Version="2.0.0-3.beta" />
18+
<PackageReference Include="CSF.Extensions.WebDriver" Version="2.0.0-4.beta" />
1919
<PackageReference Include="CSF.Specifications" Version="2.0.0" />
2020
</ItemGroup>
2121

CSF.Screenplay.Selenium/PerformableBuilder.elementPerformables.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using CSF.Screenplay.Selenium.Actions;
23
using CSF.Screenplay.Selenium.Builders;
34
using CSF.Screenplay.Selenium.Elements;
@@ -32,6 +33,18 @@ public static partial class PerformableBuilder
3233
/// <returns>A builder with which the user may select a target element.</returns>
3334
public static SendKeysBuilder EnterTheText(params string[] text) => new SendKeysBuilder(string.Join(string.Empty, text));
3435

36+
/// <summary>
37+
/// 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.
38+
/// </summary>
39+
/// <remarks>
40+
/// <para>
41+
/// If the specified <paramref name="date"/> is <see langword="null"/> then the input element will be cleared.
42+
/// </para>
43+
/// </remarks>
44+
/// <param name="date">The date to enter into the input control.</param>
45+
/// <returns>A builder with which the user may select a target element and optionally a culture.</returns>
46+
public static EnterTheDateBuilder EnterTheDate(DateTime? date) => new EnterTheDateBuilder(date);
47+
3548
/// <summary>
3649
/// Gets a performable which represents an actor deselecting everything from a <c>&lt;select&gt;</c> element.
3750
/// </summary>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Text.RegularExpressions;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using CSF.Screenplay.Selenium.Elements;
7+
using static CSF.Screenplay.Selenium.PerformableBuilder;
8+
9+
namespace CSF.Screenplay.Selenium.Actions
10+
{
11+
/// <summary>
12+
/// A <see cref="IPerformable"/> which represents an actor entering a date value into an <c>&lt;input type="date"&gt;</c> element.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// 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
17+
/// running.
18+
/// If no culture information is specified then this task defaults to the current culture: <see cref="CultureInfo.CurrentCulture"/>.
19+
/// However, this is not certain to be correct, particularly in remote/cloud configurations where the web browser is operating on different
20+
/// infrastructure to the computer which is executing the Screenplay performance. These two computers might be operating in different cultures.
21+
/// </para>
22+
/// <para>
23+
/// If the date specified to this task is <see langword="null"/> then this task will clear the date from the target.
24+
/// </para>
25+
/// </remarks>
26+
/// <example>
27+
/// <para>
28+
/// For example, a British English browser <c>en-GB</c> expects dates to be entered in the format ddMMyyyy.
29+
/// However, a US English browser <c>en-US</c> expects dates to be entered in the format MMddyyyy.
30+
/// </para>
31+
/// </example>
32+
public class EnterTheDate : IPerformable, ICanReport
33+
{
34+
const string nonNumericPattern = @"\D";
35+
36+
static readonly Regex nonNumeric = new Regex(nonNumericPattern,
37+
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant,
38+
TimeSpan.FromMilliseconds(100));
39+
40+
readonly DateTime? date;
41+
readonly ITarget target;
42+
readonly CultureInfo culture;
43+
44+
string GetShortDatePattern() => culture.DateTimeFormat.ShortDatePattern;
45+
46+
string FormatDate() => date.HasValue ? date.Value.ToString(GetShortDatePattern()) : null;
47+
48+
/// <inheritdoc/>
49+
public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
50+
{
51+
if(!date.HasValue)
52+
return actor.PerformAsync(ClearTheContentsOf(target), cancellationToken);
53+
54+
var dateText = nonNumeric.Replace(FormatDate(), string.Empty);
55+
return actor.PerformAsync(EnterTheText(dateText).Into(target), cancellationToken);
56+
}
57+
58+
/// <inheritdoc/>
59+
public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
60+
{
61+
return date.HasValue
62+
? formatter.Format("{Actor} enters the date {Date} into {Target}", actor.Name, FormatDate(), target)
63+
: formatter.Format("{Actor} clears the date from {Target}", actor.Name, date, target);
64+
}
65+
66+
/// <summary>
67+
/// Initializes a new instance of the <see cref="EnterTheDate"/> class with the specified date.
68+
/// </summary>
69+
/// <param name="date">The date to enter into the element.</param>
70+
/// <param name="target">The element into which to enter the data</param>
71+
/// <param name="culture">The culture for which to enter the date</param>
72+
public EnterTheDate(DateTime? date, ITarget target, CultureInfo culture = null)
73+
{
74+
this.date = date;
75+
this.target = target ?? throw new ArgumentNullException(nameof(target));
76+
this.culture = culture ?? CultureInfo.CurrentCulture;
77+
}
78+
}
79+
}

CSF.Screenplay/ScreenplayExtensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,19 @@ public static ScopeAndPerformance CreateScopedPerformance(this Screenplay screen
208208
return new ScopeAndPerformance(performance, scope);
209209
}
210210

211+
/// <summary>
212+
/// Gets the event bus from the screenplay's service provider.
213+
/// </summary>
214+
/// <param name="screenplay">The screenplay from which to retrieve the event bus.</param>
215+
/// <returns>The <see cref="IHasPerformanceEvents"/> event bus instance.</returns>
216+
/// <exception cref="ArgumentNullException">If <paramref name="screenplay"/> is <see langword="null" />.</exception>
217+
public static IHasPerformanceEvents GetEventBus(this Screenplay screenplay)
218+
{
219+
if (screenplay is null)
220+
throw new ArgumentNullException(nameof(screenplay));
221+
return screenplay.ServiceProvider.GetRequiredService<IHasPerformanceEvents>();
222+
}
223+
211224
static AsyncPerformanceLogic GetAsyncPerformanceLogic(SyncPerformanceLogic syncPerformanceLogic)
212225
{
213226
return (services, token) =>

0 commit comments

Comments
 (0)