Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
db0d18e
WIP #71 - Scaffold for sample app
Jun 15, 2026
49be3be
WIP on the todo app
craigfowler Jun 16, 2026
4754e8f
WIP #71 - add initial code for sample app
Jun 22, 2026
9e254a7
#71 - Scaffold a basic runnable app
craigfowler Sep 12, 2026
1c3590c
#71 - Upgrades
craigfowler Sep 12, 2026
076acd5
#71 - WIP on the sample app
craigfowler Sep 12, 2026
9565b33
#71 - Last commit before a change of tack
craigfowler Nov 3, 2026
1984bb7
#71 - Remove logic that I won't be using
craigfowler Nov 3, 2026
d7649db
#71 - Add git submodule
craigfowler Nov 3, 2026
e1ef260
Fix infinite "doom loop" bug
craigfowler Nov 7, 2026
5253dd2
Fix some DI related issues
craigfowler Nov 7, 2026
d51307e
WIP #71 - Add sample test project
craigfowler Nov 7, 2026
7732e52
#71 - Improvements to Reqnroll integration
craigfowler Nov 8, 2026
f734d87
#71 - Improvements to sample app tests
craigfowler Nov 8, 2026
894a926
Add todomvc to VS Code tasks
craigfowler Nov 8, 2026
b441466
WIP #71 - Add more test cases
craigfowler Nov 8, 2026
1d8e3c4
Add signpost to sample project
craigfowler Nov 9, 2026
df77605
WIP #71 - Fix a typo
craigfowler Nov 9, 2026
4052ed1
WIP #71 - Further tests
craigfowler Nov 9, 2026
2d7142f
WIP #71 - annotate the sample tests
craigfowler Nov 9, 2026
e5c50f9
WIP #71 - Add sample project tests to CI
craigfowler Nov 10, 2026
d0425f3
WIP #71 - Add error-catching
craigfowler Nov 10, 2026
5c3ab16
WIP #71 - Attempt to diagnose CI failure
craigfowler Nov 10, 2026
bd211fc
Remove junk code
craigfowler Nov 10, 2026
b7501ee
Fix mistake in build script
craigfowler Nov 10, 2026
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
4 changes: 4 additions & 0 deletions .github/actions/restore-dependencies/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ runs:
working-directory: CSF.Screenplay.JsonToHtmlReport.Template/src
run: npm ci
shell: bash
- name: Restore Sample project Node modules
working-directory: Samples/CSF.Screenplay.Samples.SimpleToDoApp
run: npm ci
shell: bash
18 changes: 17 additions & 1 deletion .github/actions/run-all-tests-with-coverage/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,29 @@ runs:
echo "One or more tests have failed; this build should eventually fail"
echo "failures=true" >> "$GITHUB_OUTPUT"
fi
- name: Run sample project tests
env:
Configuration: ${{ inputs.configuration }}
Tfm: ${{ inputs.tfm }}
DISPLAY: ${{ inputs.x-display }}
id: sample_project_tests
shell: bash {0}
run: |
dotnet test Samples/CSF.Screenplay.Samples.SimpleToDoAppTests -c $Configuration --no-build --logger:nunit --test-adapter-path:.
exitCode=$?
if [ $exitCode -ne 0 ]
then
echo "One or more tests have failed; this build should eventually fail"
echo "failures=true" >> "$GITHUB_OUTPUT"
fi
- name: Emit output
id: emit_output
env:
DotnetFailures: ${{ steps.dotnet_tests.outputs.failures }}
JsFailures: ${{ steps.js_tests.outputs.failures }}
SampleProjFailures: ${{ steps.sample_project_tests.outputs.failures }}
run: |
if [ "$DotnetFailures" = 'true' -o "$JsFailures" = 'true' ]
if [ "$DotnetFailures" = 'true' -o "$JsFailures" = 'true' -o "$SampleProjFailures" = 'true' ]
then
echo "Emitting failure"
echo "failures=true" >> "$GITHUB_OUTPUT"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/dotnetCi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
with:
fetch-depth: 0
lfs: true
submodules: recursive
- name: Setup build dependencies
uses: ./.github/actions/setup-build-dependencies
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "Samples/CSF.Screenplay.Samples.SimpleToDoApp/todomvc"]
path = Samples/CSF.Screenplay.Samples.SimpleToDoApp/todomvc
url = https://github.com/tastejs/todomvc
21 changes: 17 additions & 4 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"command": "npm",
"args": ["run", "test"],
"problemMatcher": [],
"label": "npm: test (Javascript)",
"label": "npm: test (JsonToHtmlReport)",
"options": {
"cwd": "${workspaceFolder}/CSF.Screenplay.JsonToHtmlReport.Template/src"
}
Expand Down Expand Up @@ -88,7 +88,7 @@
"command": "npm",
"args": ["i"],
"problemMatcher": [],
"label": "npm: install (Javascript)",
"label": "npm: install (JsonToHtmlReport)",
"options": {
"cwd": "${workspaceFolder}/CSF.Screenplay.JsonToHtmlReport.Template/src"
}
Expand All @@ -101,7 +101,7 @@
"command": "npm",
"args": ["run", "dev"],
"problemMatcher": [],
"label": "npm: dev (Javascript)",
"label": "npm: dev (JsonToHtmlReport)",
"options": {
"cwd": "${workspaceFolder}/CSF.Screenplay.JsonToHtmlReport.Template/src"
}
Expand All @@ -114,10 +114,23 @@
"command": "npm",
"args": ["run", "build"],
"problemMatcher": [],
"label": "npm: build (Javascript)",
"label": "npm: build (JsonToHtmlReport)",
"options": {
"cwd": "${workspaceFolder}/CSF.Screenplay.JsonToHtmlReport.Template/src"
}
},
{
"group": {
"kind": "none"
},
"type": "shell",
"command": "npm",
"args": ["run", "serve"],
"problemMatcher": [],
"label": "npm: serve (TodoMVC)",
"options": {
"cwd": "${workspaceFolder}/Samples/CSF.Screenplay.Samples.SimpleToDoApp"
}
}
]
}
2 changes: 1 addition & 1 deletion CSF.Screenplay.Abstractions/Abilities/UseAStopwatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace CSF.Screenplay.Abilities
/// <remarks>
/// <para>
/// Use this ability with the actions which exposed by
/// <see cref="CSF.Screenplay.PerforamableBuilder"/>.
/// <see cref="CSF.Screenplay.PerformableBuilder"/>.
/// This ability wraps a <see cref="System.Diagnostics.Stopwatch"/> instance, allowing the actor
/// to control &amp; read it from the related actions.
/// </para>
Expand Down
2 changes: 1 addition & 1 deletion CSF.Screenplay.Abstractions/PerformableBuilder.Assets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace CSF.Screenplay
/// This will allow you use the method names in this class in a more human-readable fashion.
/// </para>
/// </remarks>
public static partial class PerforamableBuilder
public static partial class PerformableBuilder
{
/// <summary>
/// Gets a builder for an Action which copies a file at the specified source path into the assets for the current performance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace CSF.Screenplay
{
public static partial class PerforamableBuilder
public static partial class PerformableBuilder
{
/// <summary>
/// Gets a performable which starts the stopwatch.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace CSF.Screenplay.Performables
/// <remarks>
/// <para>
/// This performable requires the actor has the ability <see cref="UseAStopwatch"/>.
/// Use this performable via the builder method <see cref="PerforamableBuilder.ReadTheStopwatch"/>.
/// Use this performable via the builder method <see cref="PerformableBuilder.ReadTheStopwatch"/>.
/// </para>
/// </remarks>
public class ReadTheStopwatch : IPerformableWithResult<TimeSpan>, ICanReport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace CSF.Screenplay.Performables
/// <remarks>
/// <para>
/// This performable requires the actor has the ability <see cref="UseAStopwatch"/>.
/// Use this performable via the builder method <see cref="PerforamableBuilder.ResetTheStopwatch"/>.
/// Use this performable via the builder method <see cref="PerformableBuilder.ResetTheStopwatch"/>.
/// </para>
/// </remarks>
public class ResetTheStopwatch : IPerformable, ICanReport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace CSF.Screenplay.Performables
/// <remarks>
/// <para>
/// This performable requires the actor has the ability <see cref="UseAStopwatch"/>.
/// Use this performable via the builder method <see cref="PerforamableBuilder.StartTheStopwatch"/>.
/// Use this performable via the builder method <see cref="PerformableBuilder.StartTheStopwatch"/>.
/// </para>
/// </remarks>
public class StartTheStopwatch : IPerformable, ICanReport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace CSF.Screenplay.Performables
/// <remarks>
/// <para>
/// This performable requires the actor has the ability <see cref="UseAStopwatch"/>.
/// Use this performable via the builder method <see cref="PerforamableBuilder.StopTheStopwatch"/>.
/// Use this performable via the builder method <see cref="PerformableBuilder.StopTheStopwatch"/>.
/// </para>
/// </remarks>
public class StopTheStopwatch : IPerformable, ICanReport
Expand Down
4 changes: 4 additions & 0 deletions CSF.Screenplay.Docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ If you'd like, you are also encouraged to [write your own extension], adding new
[extensions]: ../glossary/Extension.md
[a list of the extensions which are authored alongside Screenplay's core]: extensions/index.md
[write your own extension]: extendingScreenplay/index.md
## A sample Screenplay project
[The `Samples` directory within the git repository] contains a small UI-only web application and a Screenplay test project which tests it. The tests make use of the Reqnroll test framework and Selenium extension, to test the web application.

[The `Samples` directory within the git repository]: https://github.com/csf-dev/CSF.Screenplay/tree/master/Samples
4 changes: 4 additions & 0 deletions CSF.Screenplay.Docs/glossary/Persona.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
uid: PersonaGlossaryItem
---

# Persona

A Persona is a class which serves as [a factory] for a specific **[Actor]**.
Expand Down
2 changes: 1 addition & 1 deletion CSF.Screenplay.JsonToHtmlReport.Template/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# HTML template source

This project contains a small node/Webpack project, used to build the HTML file template for **CSF.Screenplay.JsonToHtmlReport**.
This project contains a small node/Vite project, used to build the HTML file template for **CSF.Screenplay.JsonToHtmlReport**.

The HTML template is used as an embedded resource in that app/library.
This project contains the _source code_ for building the HTML template; the build output of this project is sent to `CSF.Screenplay.JsonToHtmlReport/template/template.html`, where it is consumed as an embedded resource.
Expand Down
1 change: 1 addition & 0 deletions CSF.Screenplay.Reqnroll/CSF.Screenplay.Reqnroll.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<ItemGroup>
<PackageReference Include="Reqnroll" Condition="'$(ReqnrollVersion)' == ''" Version="2.0.0" />
<PackageReference Include="Reqnroll" Condition="'$(ReqnrollVersion)' != ''" Version="$(ReqnrollVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions " Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
58 changes: 57 additions & 1 deletion CSF.Screenplay.Reqnroll/ServiceProviderAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using Microsoft.Extensions.Logging;

#if SPECFLOW
using BoDi;
#else
Expand All @@ -16,7 +18,61 @@ public class ServiceProviderAdapter : IServiceProvider
readonly IObjectContainer wrapped;

/// <inheritdoc/>
public object GetService(Type serviceType) => wrapped.Resolve(serviceType);
/// <remarks>
/// <para>
/// The design of BoDi means that the <c>ObjectContainer.IsRegistered</c> method (and variants) are unsafe to use here.
/// This means that we cannot reliably check whether a service type is already available in the container in a side-effect free manner.
/// This is because when using <c>IsRegistered</c> or its other overloads, can return <see langword="false"/> for types which BoDi was capable
/// of resolving, due to its <em>registration-free resolution</em> functionality. That is - when resolving concrete types which are not registered, it
/// will do its best to create an instance regardless.
/// Thus, whilst <c>IsRegistered</c> is correctly named, it is not what we want here. What we wanted is "Can you resolve type X?"
/// </para>
/// <para>
/// The alternative, used here, is to attempt to resolve the type, but contained within a <c>try/catch</c> which catched BoDi's
/// <see cref="ObjectContainerException"/>, which is thrown if BoDi cannot resolve the specified type. At that point, if it can't be resolved,
/// then we use the <c>catch</c> to revert to the <see cref="IServiceProvider"/> behaviour, which is to return <see langword="null"/> for a service
/// which cannot be resolved.
/// </para>
/// <para>
/// <b>Beware when debugging</b> that this comes with an unfortunate trade-off! If BoDi encounters an error whilst resolving something, then we can't tell at this
/// point whether that was an expected or unexpected error. An example of an expected error would be that we're trying to resolve an interface which
/// is not registered. An unexpected error might be an exception encountered within the constructor of something we are resolving.
/// Because the contract of <see cref="IServiceProvider"/> is to return null when attempting to resolve things which are not registered, then we must not
/// throw for those expected errors. The dilemma this offers us (because we can't tell expected and unexpected apart) is that we either have to throw for
/// every problem, or never throw.
/// I have picked the "never throw" route, but the trade-off is that this logic masks unexpected errors. That will make debugging more difficult, because unexpected
/// exceptions from BoDi will be squashed and replaced with null returns. What I have done to make that less painful is to integrate with MS Logging Abstractions here.
/// If you install and register a logging abstraction within DI, then an implementation of <see cref="ILogger{T}"/> will be resolved and the original error
/// recorded in the log.
/// </para>
/// </remarks>
public object GetService(Type serviceType)
{
try
{
return wrapped.Resolve(serviceType);
}
catch(ObjectContainerException ex)
{
if(TryGetLogger(out var logger))
logger.LogError(ex, "An error occured within BoDi's DI resolution. As we cannot differentiate between expected and unexpected errors, the service provider adapter logs it and returns null.");
return null;
}
}

bool TryGetLogger(out ILogger<ServiceProviderAdapter> logger)
{
try
{
logger = wrapped.Resolve<ILogger<ServiceProviderAdapter>>();
return true;
}
catch(ObjectContainerException)
{
logger = null;
return false;
}
}

/// <summary>
/// Initialises an instance of <see cref="ServiceProviderAdapter"/>.
Expand Down
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 @@ -21,7 +21,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-7.beta" />
<PackageReference Include="CSF.Extensions.WebDriver" Version="2.0.0-9.beta" />
<PackageReference Include="CSF.Specifications" Version="2.0.0" />
</ItemGroup>

Expand Down
3 changes: 3 additions & 0 deletions CSF.Screenplay.Selenium/Tasks/TakeAndSaveScreenshot.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using CSF.Screenplay.Abilities;
using OpenQA.Selenium;
using static CSF.Screenplay.Selenium.PerformableBuilder;

Expand Down Expand Up @@ -51,6 +52,8 @@ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment form
/// <inheritdoc/>
public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
{
if(!actor.HasAbility<GetAssetFilePaths>() && !throwIfUnsupported) return;

var screenshot = await actor.PerformAsync(GetTakeAScreenshotPerformable(), cancellationToken);
if(screenshot == null) return;
await actor.PerformAsync(SaveTheScreenshot(screenshot).WithTheName(name).WithTheFormat(format), cancellationToken);
Expand Down
1 change: 1 addition & 0 deletions CSF.Screenplay.SpecFlow/CSF.Screenplay.SpecFlow.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

<ItemGroup>
<PackageReference Include="SpecFlow" Version="3.4.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions " Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
11 changes: 11 additions & 0 deletions CSF.Screenplay.sln
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Screenplay.Reqnroll", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Screenplay.Reqnroll.Tests", "Tests\CSF.Screenplay.Reqnroll.Tests\CSF.Screenplay.Reqnroll.Tests.csproj", "{A3D505D9-FE84-4032-8356-B90A30A8E4C4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{9C0561D5-F438-4A32-973A-979B5378E5A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Screenplay.Samples.SimpleToDoAppTests", "Samples\CSF.Screenplay.Samples.SimpleToDoAppTests\CSF.Screenplay.Samples.SimpleToDoAppTests.csproj", "{7C44F380-B414-40D7-8B30-EA9908AD70E1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -138,6 +142,12 @@ Global
{A3D505D9-FE84-4032-8356-B90A30A8E4C4}.Release|Any CPU.Build.0 = Release|Any CPU
{A3D505D9-FE84-4032-8356-B90A30A8E4C4}.Docs|Any CPU.ActiveCfg = Debug|Any CPU
{A3D505D9-FE84-4032-8356-B90A30A8E4C4}.Docs|Any CPU.Build.0 = Debug|Any CPU
{7C44F380-B414-40D7-8B30-EA9908AD70E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C44F380-B414-40D7-8B30-EA9908AD70E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C44F380-B414-40D7-8B30-EA9908AD70E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C44F380-B414-40D7-8B30-EA9908AD70E1}.Release|Any CPU.Build.0 = Release|Any CPU
{7C44F380-B414-40D7-8B30-EA9908AD70E1}.Docs|Any CPU.ActiveCfg = Debug|Any CPU
{7C44F380-B414-40D7-8B30-EA9908AD70E1}.Docs|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7369BCFA-DA98-4C05-9708-7949AF3E485B} = {2C3ABE29-B5F7-404E-B20A-E3679E2ECEC5}
Expand All @@ -146,5 +156,6 @@ Global
{AEF2B39D-5129-41DA-AD51-02C8617A344F} = {2C3ABE29-B5F7-404E-B20A-E3679E2ECEC5}
{B5C9CDE0-9EAB-4381-8C79-D3F2ED1A09D2} = {2C3ABE29-B5F7-404E-B20A-E3679E2ECEC5}
{A3D505D9-FE84-4032-8356-B90A30A8E4C4} = {2C3ABE29-B5F7-404E-B20A-E3679E2ECEC5}
{7C44F380-B414-40D7-8B30-EA9908AD70E1} = {9C0561D5-F438-4A32-973A-979B5378E5A1}
EndGlobalSection
EndGlobal
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,18 @@ There is nothing hand-editable inside.

[![.NET CI](https://github.com/csf-dev/CSF.Screenplay/actions/workflows/dotnetCi.yml/badge.svg)](https://github.com/csf-dev/CSF.Screenplay/actions/workflows/dotnetCi.yml)
[![Test coverage](https://sonarcloud.io/api/project_badges/measure?project=csf-dev_CSF.Screenplay&metric=coverage)](https://sonarcloud.io/summary/new_code?id=csf-dev_CSF.Screenplay)

### This repository uses git submodules

Note that - for the sample application - this repository makes use of [git submodules].
When cloning this repository, you should either use the [`--recurse-submodules`] option, or after cloning use:

```txt
git submodule update --init --recursive
```

Failure to do so will leave you without [the sample web app] files.

[git submodules]: https://git-scm.com/docs/gitsubmodules
[`--recurse-submodules`]: https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---recurse-submodulespathspec
[the sample web app]: ./Samples/CSF.Screenplay.Samples.SimpleToDoApp/README.md
Binary file not shown.
34 changes: 34 additions & 0 deletions Samples/CSF.Screenplay.Samples.SimpleToDoApp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Simple TODO app

This project contains a [git submodule] which points to [the TodoMVC project].
TodoMVC is a small, self-contained web application for which we may write tests using Screenplay.
The sample app uses **version 2.0.0** of TodoMVC.

[git submodule]: https://git-scm.com/docs/gitsubmodules
[the TodoMVC project]: https://github.com/tastejs/todomvc

## If the `todomvc` directory is empty

If the directory is empty in your working copy then most likely you did not initialise the submodules, as directed [in the instructions for cloning the repo].
You can fix this by running the command:

```txt
git submodule update --init --recursive
```

[in the instructions for cloning the repo]: ../../README.md

## Running the sample app

To run the sample app, use the following commands, from this directory.

```txt
npm ci
npm run serve
```

The app will be available at `http://localhost:5173`.
You may change the port if desired from `package.json`.

Note: The test project `CSF.Screenplay.Samples.SimpleToDoAppTests` performs these steps automatically when it runs.
You only need run it manually if you'd like to see it for yourself.
Loading
Loading