diff --git a/.github/actions/restore-dependencies/action.yml b/.github/actions/restore-dependencies/action.yml
index f9f249d6..9bbd5049 100644
--- a/.github/actions/restore-dependencies/action.yml
+++ b/.github/actions/restore-dependencies/action.yml
@@ -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
diff --git a/.github/actions/run-all-tests-with-coverage/action.yml b/.github/actions/run-all-tests-with-coverage/action.yml
index b50b5835..3765135d 100644
--- a/.github/actions/run-all-tests-with-coverage/action.yml
+++ b/.github/actions/run-all-tests-with-coverage/action.yml
@@ -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"
diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml
index dd871696..a5d4ad97 100644
--- a/.github/workflows/dotnetCi.yml
+++ b/.github/workflows/dotnetCi.yml
@@ -26,6 +26,7 @@ jobs:
with:
fetch-depth: 0
lfs: true
+ submodules: recursive
- name: Setup build dependencies
uses: ./.github/actions/setup-build-dependencies
with:
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..914b54d2
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "Samples/CSF.Screenplay.Samples.SimpleToDoApp/todomvc"]
+ path = Samples/CSF.Screenplay.Samples.SimpleToDoApp/todomvc
+ url = https://github.com/tastejs/todomvc
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 7e9e591f..30706b48 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
+ }
}
]
}
\ No newline at end of file
diff --git a/CSF.Screenplay.Abstractions/Abilities/UseAStopwatch.cs b/CSF.Screenplay.Abstractions/Abilities/UseAStopwatch.cs
index de2ed081..36c927d9 100644
--- a/CSF.Screenplay.Abstractions/Abilities/UseAStopwatch.cs
+++ b/CSF.Screenplay.Abstractions/Abilities/UseAStopwatch.cs
@@ -9,7 +9,7 @@ namespace CSF.Screenplay.Abilities
///
///
/// Use this ability with the actions which exposed by
- /// .
+ /// .
/// This ability wraps a instance, allowing the actor
/// to control & read it from the related actions.
///
diff --git a/CSF.Screenplay.Abstractions/PerformableBuilder.Assets.cs b/CSF.Screenplay.Abstractions/PerformableBuilder.Assets.cs
index 62d7442e..94111137 100644
--- a/CSF.Screenplay.Abstractions/PerformableBuilder.Assets.cs
+++ b/CSF.Screenplay.Abstractions/PerformableBuilder.Assets.cs
@@ -13,7 +13,7 @@ namespace CSF.Screenplay
/// This will allow you use the method names in this class in a more human-readable fashion.
///
///
- public static partial class PerforamableBuilder
+ public static partial class PerformableBuilder
{
///
/// Gets a builder for an Action which copies a file at the specified source path into the assets for the current performance.
diff --git a/CSF.Screenplay.Abstractions/PerformableBuilder.Stopwatch.cs b/CSF.Screenplay.Abstractions/PerformableBuilder.Stopwatch.cs
index a02cc5ec..0d0d2333 100644
--- a/CSF.Screenplay.Abstractions/PerformableBuilder.Stopwatch.cs
+++ b/CSF.Screenplay.Abstractions/PerformableBuilder.Stopwatch.cs
@@ -3,7 +3,7 @@
namespace CSF.Screenplay
{
- public static partial class PerforamableBuilder
+ public static partial class PerformableBuilder
{
///
/// Gets a performable which starts the stopwatch.
diff --git a/CSF.Screenplay.Abstractions/Performables/ReadTheStopwatch.cs b/CSF.Screenplay.Abstractions/Performables/ReadTheStopwatch.cs
index ce404158..7edca29a 100644
--- a/CSF.Screenplay.Abstractions/Performables/ReadTheStopwatch.cs
+++ b/CSF.Screenplay.Abstractions/Performables/ReadTheStopwatch.cs
@@ -12,7 +12,7 @@ namespace CSF.Screenplay.Performables
///
///
/// This performable requires the actor has the ability .
- /// Use this performable via the builder method .
+ /// Use this performable via the builder method .
///
///
public class ReadTheStopwatch : IPerformableWithResult, ICanReport
diff --git a/CSF.Screenplay.Abstractions/Performables/ResetTheStopwatch.cs b/CSF.Screenplay.Abstractions/Performables/ResetTheStopwatch.cs
index ba607230..f05db09b 100644
--- a/CSF.Screenplay.Abstractions/Performables/ResetTheStopwatch.cs
+++ b/CSF.Screenplay.Abstractions/Performables/ResetTheStopwatch.cs
@@ -11,7 +11,7 @@ namespace CSF.Screenplay.Performables
///
///
/// This performable requires the actor has the ability .
- /// Use this performable via the builder method .
+ /// Use this performable via the builder method .
///
///
public class ResetTheStopwatch : IPerformable, ICanReport
diff --git a/CSF.Screenplay.Abstractions/Performables/StartTheStopwatch.cs b/CSF.Screenplay.Abstractions/Performables/StartTheStopwatch.cs
index e7c90fb9..1840b4fb 100644
--- a/CSF.Screenplay.Abstractions/Performables/StartTheStopwatch.cs
+++ b/CSF.Screenplay.Abstractions/Performables/StartTheStopwatch.cs
@@ -11,7 +11,7 @@ namespace CSF.Screenplay.Performables
///
///
/// This performable requires the actor has the ability .
- /// Use this performable via the builder method .
+ /// Use this performable via the builder method .
///
///
public class StartTheStopwatch : IPerformable, ICanReport
diff --git a/CSF.Screenplay.Abstractions/Performables/StopTheStopwatch.cs b/CSF.Screenplay.Abstractions/Performables/StopTheStopwatch.cs
index 198804c6..fb10d4ea 100644
--- a/CSF.Screenplay.Abstractions/Performables/StopTheStopwatch.cs
+++ b/CSF.Screenplay.Abstractions/Performables/StopTheStopwatch.cs
@@ -11,7 +11,7 @@ namespace CSF.Screenplay.Performables
///
///
/// This performable requires the actor has the ability .
- /// Use this performable via the builder method .
+ /// Use this performable via the builder method .
///
///
public class StopTheStopwatch : IPerformable, ICanReport
diff --git a/CSF.Screenplay.Docs/docs/index.md b/CSF.Screenplay.Docs/docs/index.md
index c652b639..61b34345 100644
--- a/CSF.Screenplay.Docs/docs/index.md
+++ b/CSF.Screenplay.Docs/docs/index.md
@@ -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
\ No newline at end of file
diff --git a/CSF.Screenplay.Docs/glossary/Persona.md b/CSF.Screenplay.Docs/glossary/Persona.md
index 8719d982..f4a93980 100644
--- a/CSF.Screenplay.Docs/glossary/Persona.md
+++ b/CSF.Screenplay.Docs/glossary/Persona.md
@@ -1,3 +1,7 @@
+---
+uid: PersonaGlossaryItem
+---
+
# Persona
A Persona is a class which serves as [a factory] for a specific **[Actor]**.
diff --git a/CSF.Screenplay.JsonToHtmlReport.Template/README.md b/CSF.Screenplay.JsonToHtmlReport.Template/README.md
index d9d23257..c13bc47b 100644
--- a/CSF.Screenplay.JsonToHtmlReport.Template/README.md
+++ b/CSF.Screenplay.JsonToHtmlReport.Template/README.md
@@ -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.
diff --git a/CSF.Screenplay.Reqnroll/CSF.Screenplay.Reqnroll.csproj b/CSF.Screenplay.Reqnroll/CSF.Screenplay.Reqnroll.csproj
index a4def43a..a9df8cc4 100644
--- a/CSF.Screenplay.Reqnroll/CSF.Screenplay.Reqnroll.csproj
+++ b/CSF.Screenplay.Reqnroll/CSF.Screenplay.Reqnroll.csproj
@@ -16,6 +16,7 @@
+
diff --git a/CSF.Screenplay.Reqnroll/ServiceProviderAdapter.cs b/CSF.Screenplay.Reqnroll/ServiceProviderAdapter.cs
index 4684d5c1..f7e50b4c 100644
--- a/CSF.Screenplay.Reqnroll/ServiceProviderAdapter.cs
+++ b/CSF.Screenplay.Reqnroll/ServiceProviderAdapter.cs
@@ -1,4 +1,6 @@
using System;
+using Microsoft.Extensions.Logging;
+
#if SPECFLOW
using BoDi;
#else
@@ -16,7 +18,61 @@ public class ServiceProviderAdapter : IServiceProvider
readonly IObjectContainer wrapped;
///
- public object GetService(Type serviceType) => wrapped.Resolve(serviceType);
+ ///
+ ///
+ /// The design of BoDi means that the ObjectContainer.IsRegistered 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 IsRegistered or its other overloads, can return for types which BoDi was capable
+ /// of resolving, due to its registration-free resolution functionality. That is - when resolving concrete types which are not registered, it
+ /// will do its best to create an instance regardless.
+ /// Thus, whilst IsRegistered is correctly named, it is not what we want here. What we wanted is "Can you resolve type X?"
+ ///
+ ///
+ /// The alternative, used here, is to attempt to resolve the type, but contained within a try/catch which catched BoDi's
+ /// , which is thrown if BoDi cannot resolve the specified type. At that point, if it can't be resolved,
+ /// then we use the catch to revert to the behaviour, which is to return for a service
+ /// which cannot be resolved.
+ ///
+ ///
+ /// Beware when debugging 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 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 will be resolved and the original error
+ /// recorded in the log.
+ ///
+ ///
+ 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 logger)
+ {
+ try
+ {
+ logger = wrapped.Resolve>();
+ return true;
+ }
+ catch(ObjectContainerException)
+ {
+ logger = null;
+ return false;
+ }
+ }
///
/// Initialises an instance of .
diff --git a/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj b/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj
index cc241118..48f15482 100644
--- a/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj
+++ b/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/CSF.Screenplay.Selenium/Tasks/TakeAndSaveScreenshot.cs b/CSF.Screenplay.Selenium/Tasks/TakeAndSaveScreenshot.cs
index db808c9a..d3dc4653 100644
--- a/CSF.Screenplay.Selenium/Tasks/TakeAndSaveScreenshot.cs
+++ b/CSF.Screenplay.Selenium/Tasks/TakeAndSaveScreenshot.cs
@@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
+using CSF.Screenplay.Abilities;
using OpenQA.Selenium;
using static CSF.Screenplay.Selenium.PerformableBuilder;
@@ -51,6 +52,8 @@ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment form
///
public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
{
+ if(!actor.HasAbility() && !throwIfUnsupported) return;
+
var screenshot = await actor.PerformAsync(GetTakeAScreenshotPerformable(), cancellationToken);
if(screenshot == null) return;
await actor.PerformAsync(SaveTheScreenshot(screenshot).WithTheName(name).WithTheFormat(format), cancellationToken);
diff --git a/CSF.Screenplay.SpecFlow/CSF.Screenplay.SpecFlow.csproj b/CSF.Screenplay.SpecFlow/CSF.Screenplay.SpecFlow.csproj
index fc6dce5d..31a8e2db 100644
--- a/CSF.Screenplay.SpecFlow/CSF.Screenplay.SpecFlow.csproj
+++ b/CSF.Screenplay.SpecFlow/CSF.Screenplay.SpecFlow.csproj
@@ -16,6 +16,7 @@
+
diff --git a/CSF.Screenplay.sln b/CSF.Screenplay.sln
index 4f45b46d..1598a71b 100644
--- a/CSF.Screenplay.sln
+++ b/CSF.Screenplay.sln
@@ -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
@@ -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}
@@ -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
diff --git a/README.md b/README.md
index 8c89e5d3..41df1f04 100755
--- a/README.md
+++ b/README.md
@@ -27,3 +27,18 @@ There is nothing hand-editable inside.
[](https://github.com/csf-dev/CSF.Screenplay/actions/workflows/dotnetCi.yml)
[](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
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoApp/.node-version b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/.node-version
new file mode 100644
index 00000000..9c173dc9
Binary files /dev/null and b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/.node-version differ
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoApp/README.md b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/README.md
new file mode 100644
index 00000000..dd4c184c
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/README.md
@@ -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.
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoApp/package-lock.json b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/package-lock.json
new file mode 100644
index 00000000..ac738bf4
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/package-lock.json
@@ -0,0 +1,645 @@
+{
+ "name": "simpletodoapp",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "simpletodoapp",
+ "version": "1.0.0",
+ "license": "MIT",
+ "devDependencies": {
+ "http-server": "^14.1.1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/corser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
+ "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz",
+ "integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-server": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
+ "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "^2.0.1",
+ "chalk": "^4.1.2",
+ "corser": "^2.0.1",
+ "he": "^1.2.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy": "^1.18.1",
+ "mime": "^1.6.0",
+ "minimist": "^1.2.6",
+ "opener": "^1.5.1",
+ "portfinder": "^1.0.28",
+ "secure-compare": "3.0.1",
+ "union": "~0.5.0",
+ "url-join": "^4.0.1"
+ },
+ "bin": {
+ "http-server": "bin/http-server"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/opener": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+ "dev": true,
+ "license": "(WTFPL OR MIT)",
+ "bin": {
+ "opener": "bin/opener-bin.js"
+ }
+ },
+ "node_modules/portfinder": {
+ "version": "1.0.38",
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
+ "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async": "^3.2.6",
+ "debug": "^4.3.6"
+ },
+ "engines": {
+ "node": ">= 10.12"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.17.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.1.tgz",
+ "integrity": "sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/secure-compare": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
+ "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/union": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
+ "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
+ "dev": true,
+ "dependencies": {
+ "qs": "^6.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoApp/package.json b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/package.json
new file mode 100644
index 00000000..451baca1
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "simpletodoapp",
+ "version": "1.0.0",
+ "description": "A simple To Do List application, which will have tests written in CSF.Screenplay & Reqnroll",
+ "scripts": {
+ "serve": "http-server ./todomvc/examples/javascript-es6/dist -p 5173 -c-1 --cors"
+ },
+ "homepage": "https://github.com/csf-dev/CSF.Screenplay#readme",
+ "bugs": {
+ "url": "https://github.com/csf-dev/CSF.Screenplay/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/csf-dev/CSF.Screenplay.git"
+ },
+ "license": "MIT",
+ "author": "craigfowler",
+ "devDependencies": {
+ "http-server": "^14.1.1"
+ }
+}
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoApp/todomvc b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/todomvc
new file mode 160000
index 00000000..53b5b616
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoApp/todomvc
@@ -0,0 +1 @@
+Subproject commit 53b5b616c2b456ca980aa463c9b5f0e6a0a75bc6
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CSF.Screenplay.Samples.SimpleToDoAppTests.csproj b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CSF.Screenplay.Samples.SimpleToDoAppTests.csproj
new file mode 100644
index 00000000..7f8920ce
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CSF.Screenplay.Samples.SimpleToDoAppTests.csproj
@@ -0,0 +1,41 @@
+
+
+
+ net8.0
+ enable
+ false
+ true
+ CSF.Screenplay
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CiChromeOptionsCustomizer.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CiChromeOptionsCustomizer.cs
new file mode 100644
index 00000000..86d4bf95
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CiChromeOptionsCustomizer.cs
@@ -0,0 +1,21 @@
+using CSF.Extensions.WebDriver.Factories;
+using OpenQA.Selenium.Chrome;
+
+namespace CSF.Screenplay;
+
+///
+/// A customization class which sets up Google Chrome browser with some headless options.
+///
+///
+///
+/// This is not strictly part of Screenplay or its Selenium plugin. This is used with ,
+/// to pre-configure a browser.
+///
+///
+public class CiChromeOptionsCustomizer : ICustomizesOptions
+{
+ public void CustomizeOptions(ChromeOptions options)
+ {
+ options.AddArguments("--disable-dev-shm-usage", "--headless", "--no-sandbox", "--disable-gpu", "--window-size=1280,1024");
+ }
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CreateTodos.feature b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CreateTodos.feature
new file mode 100644
index 00000000..43754409
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CreateTodos.feature
@@ -0,0 +1,28 @@
+Feature: Creating some Todo items
+
+ Users should be able to create a number of Todo items.
+ Each item is independent and adds to the list of displayed items.
+
+Background:
+ Given Webster has opened the todo app
+
+Scenario: A user can create a single todo and it appears in the list
+ When he creates a todo item reading feed the cat
+ Then the count of displayed todos should be 1
+
+Scenario: A user can create three todos and they all appear in the list
+ When he creates several todo items
+ | Item |
+ | feed the cat |
+ | feed the dog |
+ | buy milk |
+ Then the count of displayed todos should be 3
+
+Scenario: Todo items should be listed in inverse order in which they are created
+ When he creates several todo items
+ | Item |
+ | feed the cat |
+ | feed the dog |
+ | buy milk |
+ Then the item displayed at position 3 should read feed the cat
+ And the item displayed at position 1 should read buy milk
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CreateTodos.feature.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CreateTodos.feature.cs
new file mode 100644
index 00000000..2c0abd32
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CreateTodos.feature.cs
@@ -0,0 +1,197 @@
+// ------------------------------------------------------------------------------
+//
+// This code was generated by Reqnroll (https://www.reqnroll.net/).
+// Reqnroll Version:2.0.0.0
+// Reqnroll Generator Version:2.0.0.0
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+// ------------------------------------------------------------------------------
+#region Designer generated code
+#pragma warning disable
+namespace CSF.Screenplay
+{
+ using Reqnroll;
+ using System;
+ using System.Linq;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Reqnroll", "2.0.0.0")]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [NUnit.Framework.TestFixtureAttribute()]
+ [NUnit.Framework.DescriptionAttribute("Creating some Todo items")]
+ public partial class CreatingSomeTodoItemsFeature
+ {
+
+ private global::Reqnroll.ITestRunner testRunner;
+
+ private static string[] featureTags = ((string[])(null));
+
+#line 1 "CreateTodos.feature"
+#line hidden
+
+ [NUnit.Framework.OneTimeSetUpAttribute()]
+ public virtual async System.Threading.Tasks.Task FeatureSetupAsync()
+ {
+ testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, NUnit.Framework.TestContext.CurrentContext.WorkerId);
+ global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "", "Creating some Todo items", " Users should be able to create a number of Todo items.\r\n Each item is inde" +
+ "pendent and adds to the list of displayed items.", global::Reqnroll.ProgrammingLanguage.CSharp, featureTags);
+ await testRunner.OnFeatureStartAsync(featureInfo);
+ }
+
+ [NUnit.Framework.OneTimeTearDownAttribute()]
+ public virtual async System.Threading.Tasks.Task FeatureTearDownAsync()
+ {
+ await testRunner.OnFeatureEndAsync();
+ testRunner = null;
+ }
+
+ [NUnit.Framework.SetUpAttribute()]
+ public async System.Threading.Tasks.Task TestInitializeAsync()
+ {
+ }
+
+ [NUnit.Framework.TearDownAttribute()]
+ public async System.Threading.Tasks.Task TestTearDownAsync()
+ {
+ await testRunner.OnScenarioEndAsync();
+ }
+
+ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo)
+ {
+ testRunner.OnScenarioInitialize(scenarioInfo);
+ testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext);
+ }
+
+ public async System.Threading.Tasks.Task ScenarioStartAsync()
+ {
+ await testRunner.OnScenarioStartAsync();
+ }
+
+ public async System.Threading.Tasks.Task ScenarioCleanupAsync()
+ {
+ await testRunner.CollectScenarioErrorsAsync();
+ }
+
+ public virtual async System.Threading.Tasks.Task FeatureBackgroundAsync()
+ {
+#line 6
+#line hidden
+#line 7
+ await testRunner.GivenAsync("Webster has opened the todo app", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given ");
+#line hidden
+ }
+
+ [NUnit.Framework.TestAttribute()]
+ [NUnit.Framework.DescriptionAttribute("A user can create a single todo and it appears in the list")]
+ public async System.Threading.Tasks.Task AUserCanCreateASingleTodoAndItAppearsInTheList()
+ {
+ string[] tagsOfScenario = ((string[])(null));
+ System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
+ global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("A user can create a single todo and it appears in the list", null, tagsOfScenario, argumentsOfScenario, featureTags);
+#line 9
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 6
+await this.FeatureBackgroundAsync();
+#line hidden
+#line 10
+ await testRunner.WhenAsync("he creates a todo item reading feed the cat", ((string)(null)), ((global::Reqnroll.Table)(null)), "When ");
+#line hidden
+#line 11
+ await testRunner.ThenAsync("the count of displayed todos should be 1", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then ");
+#line hidden
+ }
+ await this.ScenarioCleanupAsync();
+ }
+
+ [NUnit.Framework.TestAttribute()]
+ [NUnit.Framework.DescriptionAttribute("A user can create three todos and they all appear in the list")]
+ public async System.Threading.Tasks.Task AUserCanCreateThreeTodosAndTheyAllAppearInTheList()
+ {
+ string[] tagsOfScenario = ((string[])(null));
+ System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
+ global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("A user can create three todos and they all appear in the list", null, tagsOfScenario, argumentsOfScenario, featureTags);
+#line 13
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 6
+await this.FeatureBackgroundAsync();
+#line hidden
+ global::Reqnroll.Table table1 = new global::Reqnroll.Table(new string[] {
+ "Item"});
+ table1.AddRow(new string[] {
+ "feed the cat"});
+ table1.AddRow(new string[] {
+ "feed the dog"});
+ table1.AddRow(new string[] {
+ "buy milk"});
+#line 14
+ await testRunner.WhenAsync("he creates several todo items", ((string)(null)), table1, "When ");
+#line hidden
+#line 19
+ await testRunner.ThenAsync("the count of displayed todos should be 3", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then ");
+#line hidden
+ }
+ await this.ScenarioCleanupAsync();
+ }
+
+ [NUnit.Framework.TestAttribute()]
+ [NUnit.Framework.DescriptionAttribute("Todo items should be listed in inverse order in which they are created")]
+ public async System.Threading.Tasks.Task TodoItemsShouldBeListedInInverseOrderInWhichTheyAreCreated()
+ {
+ string[] tagsOfScenario = ((string[])(null));
+ System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
+ global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Todo items should be listed in inverse order in which they are created", null, tagsOfScenario, argumentsOfScenario, featureTags);
+#line 21
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 6
+await this.FeatureBackgroundAsync();
+#line hidden
+ global::Reqnroll.Table table2 = new global::Reqnroll.Table(new string[] {
+ "Item"});
+ table2.AddRow(new string[] {
+ "feed the cat"});
+ table2.AddRow(new string[] {
+ "feed the dog"});
+ table2.AddRow(new string[] {
+ "buy milk"});
+#line 22
+ await testRunner.WhenAsync("he creates several todo items", ((string)(null)), table2, "When ");
+#line hidden
+#line 27
+ await testRunner.ThenAsync("the item displayed at position 3 should read feed the cat", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then ");
+#line hidden
+#line 28
+ await testRunner.AndAsync("the item displayed at position 1 should read buy milk", ((string)(null)), ((global::Reqnroll.Table)(null)), "And ");
+#line hidden
+ }
+ await this.ScenarioCleanupAsync();
+ }
+ }
+}
+#pragma warning restore
+#endregion
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Filtering.feature b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Filtering.feature
new file mode 100644
index 00000000..f413c11f
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Filtering.feature
@@ -0,0 +1,30 @@
+Feature: Filtering the todo list
+
+ Users should be able to filter their todo items.
+
+Background:
+ Given Webster has opened the todo app
+ And he has created several todo items
+ | Item |
+ | feed the cat |
+ | feed the dog |
+ | buy milk |
+
+Scenario: The Active filter should not show completed items
+ When he marks the item at position 1 as completed
+ And he marks the item at position 2 as completed
+ And he clicks the filter named Active
+ Then the count of displayed todos should be 1
+
+Scenario: The Completed filter should not show completed items
+ When he marks the item at position 1 as completed
+ And he marks the item at position 2 as completed
+ And he clicks the filter named Completed
+ Then the count of displayed todos should be 2
+
+Scenario: The All filter should show all items
+ When he marks the item at position 1 as completed
+ And he marks the item at position 2 as completed
+ And he clicks the filter named Completed
+ And he clicks the filter named All
+ Then the count of displayed todos should be 3
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Filtering.feature.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Filtering.feature.cs
new file mode 100644
index 00000000..c637dd55
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Filtering.feature.cs
@@ -0,0 +1,209 @@
+// ------------------------------------------------------------------------------
+//
+// This code was generated by Reqnroll (https://www.reqnroll.net/).
+// Reqnroll Version:2.0.0.0
+// Reqnroll Generator Version:2.0.0.0
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+// ------------------------------------------------------------------------------
+#region Designer generated code
+#pragma warning disable
+namespace CSF.Screenplay
+{
+ using Reqnroll;
+ using System;
+ using System.Linq;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Reqnroll", "2.0.0.0")]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [NUnit.Framework.TestFixtureAttribute()]
+ [NUnit.Framework.DescriptionAttribute("Filtering the todo list")]
+ public partial class FilteringTheTodoListFeature
+ {
+
+ private global::Reqnroll.ITestRunner testRunner;
+
+ private static string[] featureTags = ((string[])(null));
+
+#line 1 "Filtering.feature"
+#line hidden
+
+ [NUnit.Framework.OneTimeSetUpAttribute()]
+ public virtual async System.Threading.Tasks.Task FeatureSetupAsync()
+ {
+ testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, NUnit.Framework.TestContext.CurrentContext.WorkerId);
+ global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "", "Filtering the todo list", " Users should be able to filter their todo items.", global::Reqnroll.ProgrammingLanguage.CSharp, featureTags);
+ await testRunner.OnFeatureStartAsync(featureInfo);
+ }
+
+ [NUnit.Framework.OneTimeTearDownAttribute()]
+ public virtual async System.Threading.Tasks.Task FeatureTearDownAsync()
+ {
+ await testRunner.OnFeatureEndAsync();
+ testRunner = null;
+ }
+
+ [NUnit.Framework.SetUpAttribute()]
+ public async System.Threading.Tasks.Task TestInitializeAsync()
+ {
+ }
+
+ [NUnit.Framework.TearDownAttribute()]
+ public async System.Threading.Tasks.Task TestTearDownAsync()
+ {
+ await testRunner.OnScenarioEndAsync();
+ }
+
+ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo)
+ {
+ testRunner.OnScenarioInitialize(scenarioInfo);
+ testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext);
+ }
+
+ public async System.Threading.Tasks.Task ScenarioStartAsync()
+ {
+ await testRunner.OnScenarioStartAsync();
+ }
+
+ public async System.Threading.Tasks.Task ScenarioCleanupAsync()
+ {
+ await testRunner.CollectScenarioErrorsAsync();
+ }
+
+ public virtual async System.Threading.Tasks.Task FeatureBackgroundAsync()
+ {
+#line 5
+#line hidden
+#line 6
+ await testRunner.GivenAsync("Webster has opened the todo app", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given ");
+#line hidden
+ global::Reqnroll.Table table3 = new global::Reqnroll.Table(new string[] {
+ "Item"});
+ table3.AddRow(new string[] {
+ "feed the cat"});
+ table3.AddRow(new string[] {
+ "feed the dog"});
+ table3.AddRow(new string[] {
+ "buy milk"});
+#line 7
+ await testRunner.AndAsync("he has created several todo items", ((string)(null)), table3, "And ");
+#line hidden
+ }
+
+ [NUnit.Framework.TestAttribute()]
+ [NUnit.Framework.DescriptionAttribute("The Active filter should not show completed items")]
+ public async System.Threading.Tasks.Task TheActiveFilterShouldNotShowCompletedItems()
+ {
+ string[] tagsOfScenario = ((string[])(null));
+ System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
+ global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("The Active filter should not show completed items", null, tagsOfScenario, argumentsOfScenario, featureTags);
+#line 13
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 5
+await this.FeatureBackgroundAsync();
+#line hidden
+#line 14
+ await testRunner.WhenAsync("he marks the item at position 1 as completed", ((string)(null)), ((global::Reqnroll.Table)(null)), "When ");
+#line hidden
+#line 15
+ await testRunner.AndAsync("he marks the item at position 2 as completed", ((string)(null)), ((global::Reqnroll.Table)(null)), "And ");
+#line hidden
+#line 16
+ await testRunner.AndAsync("he clicks the filter named Active", ((string)(null)), ((global::Reqnroll.Table)(null)), "And ");
+#line hidden
+#line 17
+ await testRunner.ThenAsync("the count of displayed todos should be 1", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then ");
+#line hidden
+ }
+ await this.ScenarioCleanupAsync();
+ }
+
+ [NUnit.Framework.TestAttribute()]
+ [NUnit.Framework.DescriptionAttribute("The Completed filter should not show completed items")]
+ public async System.Threading.Tasks.Task TheCompletedFilterShouldNotShowCompletedItems()
+ {
+ string[] tagsOfScenario = ((string[])(null));
+ System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
+ global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("The Completed filter should not show completed items", null, tagsOfScenario, argumentsOfScenario, featureTags);
+#line 19
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 5
+await this.FeatureBackgroundAsync();
+#line hidden
+#line 20
+ await testRunner.WhenAsync("he marks the item at position 1 as completed", ((string)(null)), ((global::Reqnroll.Table)(null)), "When ");
+#line hidden
+#line 21
+ await testRunner.AndAsync("he marks the item at position 2 as completed", ((string)(null)), ((global::Reqnroll.Table)(null)), "And ");
+#line hidden
+#line 22
+ await testRunner.AndAsync("he clicks the filter named Completed", ((string)(null)), ((global::Reqnroll.Table)(null)), "And ");
+#line hidden
+#line 23
+ await testRunner.ThenAsync("the count of displayed todos should be 2", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then ");
+#line hidden
+ }
+ await this.ScenarioCleanupAsync();
+ }
+
+ [NUnit.Framework.TestAttribute()]
+ [NUnit.Framework.DescriptionAttribute("The All filter should show all items")]
+ public async System.Threading.Tasks.Task TheAllFilterShouldShowAllItems()
+ {
+ string[] tagsOfScenario = ((string[])(null));
+ System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
+ global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("The All filter should show all items", null, tagsOfScenario, argumentsOfScenario, featureTags);
+#line 25
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 5
+await this.FeatureBackgroundAsync();
+#line hidden
+#line 26
+ await testRunner.WhenAsync("he marks the item at position 1 as completed", ((string)(null)), ((global::Reqnroll.Table)(null)), "When ");
+#line hidden
+#line 27
+ await testRunner.AndAsync("he marks the item at position 2 as completed", ((string)(null)), ((global::Reqnroll.Table)(null)), "And ");
+#line hidden
+#line 28
+ await testRunner.AndAsync("he clicks the filter named Completed", ((string)(null)), ((global::Reqnroll.Table)(null)), "And ");
+#line hidden
+#line 29
+ await testRunner.AndAsync("he clicks the filter named All", ((string)(null)), ((global::Reqnroll.Table)(null)), "And ");
+#line hidden
+#line 30
+ await testRunner.ThenAsync("the count of displayed todos should be 3", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then ");
+#line hidden
+ }
+ await this.ScenarioCleanupAsync();
+ }
+ }
+}
+#pragma warning restore
+#endregion
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/GlobalUsings.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/GlobalUsings.cs
new file mode 100644
index 00000000..24c84b10
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/GlobalUsings.cs
@@ -0,0 +1,4 @@
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Reqnroll;
+global using CSF.Screenplay.Selenium;
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/OpenTheApp.feature b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/OpenTheApp.feature
new file mode 100644
index 00000000..68b5f841
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/OpenTheApp.feature
@@ -0,0 +1,5 @@
+Feature: Opening the app
+
+Scenario: Webster should be able to open the app in a web browser
+ When Webster opens the todo app
+ Then the page heading should read "todos"
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/OpenTheApp.feature.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/OpenTheApp.feature.cs
new file mode 100644
index 00000000..342ac97f
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/OpenTheApp.feature.cs
@@ -0,0 +1,105 @@
+// ------------------------------------------------------------------------------
+//
+// This code was generated by Reqnroll (https://www.reqnroll.net/).
+// Reqnroll Version:2.0.0.0
+// Reqnroll Generator Version:2.0.0.0
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+// ------------------------------------------------------------------------------
+#region Designer generated code
+#pragma warning disable
+namespace CSF.Screenplay
+{
+ using Reqnroll;
+ using System;
+ using System.Linq;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Reqnroll", "2.0.0.0")]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [NUnit.Framework.TestFixtureAttribute()]
+ [NUnit.Framework.DescriptionAttribute("Opening the app")]
+ public partial class OpeningTheAppFeature
+ {
+
+ private global::Reqnroll.ITestRunner testRunner;
+
+ private static string[] featureTags = ((string[])(null));
+
+#line 1 "OpenTheApp.feature"
+#line hidden
+
+ [NUnit.Framework.OneTimeSetUpAttribute()]
+ public virtual async System.Threading.Tasks.Task FeatureSetupAsync()
+ {
+ testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, NUnit.Framework.TestContext.CurrentContext.WorkerId);
+ global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "", "Opening the app", null, global::Reqnroll.ProgrammingLanguage.CSharp, featureTags);
+ await testRunner.OnFeatureStartAsync(featureInfo);
+ }
+
+ [NUnit.Framework.OneTimeTearDownAttribute()]
+ public virtual async System.Threading.Tasks.Task FeatureTearDownAsync()
+ {
+ await testRunner.OnFeatureEndAsync();
+ testRunner = null;
+ }
+
+ [NUnit.Framework.SetUpAttribute()]
+ public async System.Threading.Tasks.Task TestInitializeAsync()
+ {
+ }
+
+ [NUnit.Framework.TearDownAttribute()]
+ public async System.Threading.Tasks.Task TestTearDownAsync()
+ {
+ await testRunner.OnScenarioEndAsync();
+ }
+
+ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo)
+ {
+ testRunner.OnScenarioInitialize(scenarioInfo);
+ testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext);
+ }
+
+ public async System.Threading.Tasks.Task ScenarioStartAsync()
+ {
+ await testRunner.OnScenarioStartAsync();
+ }
+
+ public async System.Threading.Tasks.Task ScenarioCleanupAsync()
+ {
+ await testRunner.CollectScenarioErrorsAsync();
+ }
+
+ [NUnit.Framework.TestAttribute()]
+ [NUnit.Framework.DescriptionAttribute("Webster should be able to open the app in a web browser")]
+ public async System.Threading.Tasks.Task WebsterShouldBeAbleToOpenTheAppInAWebBrowser()
+ {
+ string[] tagsOfScenario = ((string[])(null));
+ System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
+ global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Webster should be able to open the app in a web browser", null, tagsOfScenario, argumentsOfScenario, featureTags);
+#line 3
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 4
+ await testRunner.WhenAsync("Webster opens the todo app", ((string)(null)), ((global::Reqnroll.Table)(null)), "When ");
+#line hidden
+#line 5
+ await testRunner.ThenAsync("the page heading should read \"todos\"", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then ");
+#line hidden
+ }
+ await this.ScenarioCleanupAsync();
+ }
+ }
+}
+#pragma warning restore
+#endregion
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/PerformableBuilder.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/PerformableBuilder.cs
new file mode 100644
index 00000000..884b57ff
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/PerformableBuilder.cs
@@ -0,0 +1,27 @@
+using CSF.Screenplay.Tasks;
+using static CSF.Screenplay.Selenium.PerformableBuilder;
+
+namespace CSF.Screenplay;
+
+///
+/// Builder class which runs tasks.
+///
+///
+///
+/// This class is an example of which is used to construct performable instances.
+///
+///
+public static class TodoPerformableBuilder
+{
+ public static IPerformable OpenTheApp() => NavigateTo(TodoApp.RootUri);
+
+ public static IPerformable CreateATodoReading(string text) => new CreateATodoItem(text);
+
+ public static IPerformableWithResult GetTheCountOfDisplayedTodos() => new GetTheCountOfDisplayedItems();
+
+ public static IPerformableWithResult GetTheTodoItemAtPosition(int position) => new GetTheItemDisplayedAtAPosition(position);
+
+ public static IPerformable ActivateAFilter(string name) => new ActivateAFilter(name);
+
+ public static IPerformable ToggleTheCompletedStateOfItem(int position) => new ToggleAnItemsCompletedState(position);
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/AppSteps.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/AppSteps.cs
new file mode 100644
index 00000000..857d12a4
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/AppSteps.cs
@@ -0,0 +1,33 @@
+using NUnit.Framework;
+using static CSF.Screenplay.PerformanceStarter;
+using static CSF.Screenplay.Selenium.PerformableBuilder;
+using static CSF.Screenplay.TodoPerformableBuilder;
+
+namespace CSF.Screenplay.StepDefinitions;
+
+[Binding]
+public class AppSteps(IStage stage)
+{
+ [Given("Webster has opened the todo app")]
+ public async Task GivenWebsterOpenedTheTodoApp()
+ {
+ var webster = stage.Spotlight();
+ await Given(webster).WasAbleTo(OpenTheApp());
+ }
+
+ [When("Webster opens the todo app")]
+ public async Task WhenWebsterOpensTheTodoApp()
+ {
+ var webster = stage.Spotlight();
+ await When(webster).AttemptsTo(OpenTheApp());
+ }
+
+ [Then(@"the page heading should read ""([^""]+)""")]
+ public async Task ThenThePageHeadingShouldRead(string expected)
+ {
+ var actor = stage.GetSpotlitActor();
+ var heading = await Then(actor).Should(ReadFromTheElement(TodoApp.PageHeading).TheText());
+
+ Assert.That(heading, Is.EqualTo(expected));
+ }
+}
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/FilteringSteps.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/FilteringSteps.cs
new file mode 100644
index 00000000..85fe64dd
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/FilteringSteps.cs
@@ -0,0 +1,22 @@
+using static CSF.Screenplay.PerformanceStarter;
+using static CSF.Screenplay.TodoPerformableBuilder;
+
+namespace CSF.Screenplay.StepDefinitions;
+
+[Binding]
+public class FilteringSteps(IStage stage)
+{
+ [When("(?:s?he|they) clicks? the filter named ([^\\s]+)")]
+ public async Task WhenTheyClickTheFilter(string filterName)
+ {
+ var actor = stage.GetSpotlitActor();
+ await When(actor).AttemptsTo(ActivateAFilter(filterName));
+ }
+
+ [Given("(?:s?he|they) clicked the filter named ([^\\s]+)")]
+ public async Task GivenTheyClickedTheFilter(string filterName)
+ {
+ var actor = stage.GetSpotlitActor();
+ await Given(actor).WasAbleTo(ActivateAFilter(filterName));
+ }
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/TodoSteps.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/TodoSteps.cs
new file mode 100644
index 00000000..4627c54d
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/TodoSteps.cs
@@ -0,0 +1,58 @@
+using System.Linq;
+using static CSF.Screenplay.PerformanceStarter;
+using static CSF.Screenplay.TodoPerformableBuilder;
+using NUnit.Framework;
+
+namespace CSF.Screenplay.StepDefinitions;
+
+[Binding]
+public class TodoSteps(IStage stage)
+{
+ [When("(?:s?he|they) creates? a todo item reading (.+)")]
+ public async Task WhenTheyCreateATodoItem(string text)
+ {
+ var actor = stage.GetSpotlitActor();
+ await When(actor).AttemptsTo(CreateATodoReading(text));
+ }
+
+ [When("he creates several todo items")]
+ public async Task WhenTheyCreateSomeTodoItems(DataTable dataTable)
+ {
+ var items = dataTable.Rows.Select(x => x.Values.Single()).ToArray();
+ var actor = stage.GetSpotlitActor();
+ foreach(var item in items)
+ await When(actor).AttemptsTo(CreateATodoReading(item));
+ }
+
+ [Given("he has created several todo items")]
+ public async Task GivenTheyCreatedSomeTodoItems(DataTable dataTable)
+ {
+ var items = dataTable.Rows.Select(x => x.Values.Single()).ToArray();
+ var actor = stage.GetSpotlitActor();
+ foreach(var item in items)
+ await Given(actor).WasAbleTo(CreateATodoReading(item));
+ }
+
+ [Then("the count of displayed todos should be (\\d+)")]
+ public async Task ThenAssertTheCountOfDisplayedTodos(int expected)
+ {
+ var actor = stage.GetSpotlitActor();
+ var actual = await Then(actor).Should(GetTheCountOfDisplayedTodos());
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Then("the item displayed at position (\\d+) should read (.+)")]
+ public async Task ThenTheItemAtAPositionShouldRead(int position, string expected)
+ {
+ var actor = stage.GetSpotlitActor();
+ var actual = await Then(actor).Should(GetTheTodoItemAtPosition(position));
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [When("(?:s?he|they) marks the item at position (\\d+) as completed")]
+ public async Task WhenTheyMarkAnItemAsCompleted(int position)
+ {
+ var actor = stage.GetSpotlitActor();
+ await When(actor).AttemptsTo(ToggleTheCompletedStateOfItem(position));
+ }
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/ActivateAFilter.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/ActivateAFilter.cs
new file mode 100644
index 00000000..88fb4369
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/ActivateAFilter.cs
@@ -0,0 +1,21 @@
+using System;
+using static CSF.Screenplay.Selenium.PerformableBuilder;
+
+namespace CSF.Screenplay.Tasks;
+public class ActivateAFilter(string filterName) : IPerformable, ICanReport
+{
+ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
+ => formatter.Format("{Actor} activates the filter {Name}", actor, filterName);
+
+ public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
+ {
+ var filter = filterName.ToLowerInvariant() switch
+ {
+ "all" => TodoApp.ShowAllFilter,
+ "active" => TodoApp.ActiveOnlyFilter,
+ "completed" => TodoApp.CompletedOnlyFilter,
+ _ => throw new InvalidOperationException("The filter name must be one of 'all', 'active' or 'completed' (case-invariant)")
+ };
+ return actor.PerformAsync(ClickOn(filter));
+ }
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/CreateATodoItem.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/CreateATodoItem.cs
new file mode 100644
index 00000000..dbc92340
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/CreateATodoItem.cs
@@ -0,0 +1,13 @@
+using OpenQA.Selenium;
+using static CSF.Screenplay.Selenium.PerformableBuilder;
+
+namespace CSF.Screenplay.Tasks;
+
+public class CreateATodoItem(string text) : IPerformable, ICanReport
+{
+ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
+ => formatter.Format("{Actor} creates a todo item reading \"{text}\"", actor, text);
+
+ public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
+ => actor.PerformAsync(EnterTheText(text, Keys.Enter).Into(TodoApp.NewItem), cancellationToken);
+}
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/GetTheCountOfDisplayedItems.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/GetTheCountOfDisplayedItems.cs
new file mode 100644
index 00000000..dfbf5021
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/GetTheCountOfDisplayedItems.cs
@@ -0,0 +1,15 @@
+using static CSF.Screenplay.Selenium.PerformableBuilder;
+
+namespace CSF.Screenplay.Tasks;
+
+public class GetTheCountOfDisplayedItems : IPerformableWithResult, ICanReport
+{
+ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
+ => formatter.Format("{Actor} gets the count of displayed todo items");
+
+ public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
+ {
+ var displayedTodos = await actor.PerformAsync(FindElementsOnThePage().WhichMatch(TodoApp.DisplayedTodos), cancellationToken);
+ return displayedTodos.Count;
+ }
+}
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/GetTheItemDisplayedAtAPosition.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/GetTheItemDisplayedAtAPosition.cs
new file mode 100644
index 00000000..d0a287b9
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/GetTheItemDisplayedAtAPosition.cs
@@ -0,0 +1,12 @@
+using static CSF.Screenplay.Selenium.PerformableBuilder;
+
+namespace CSF.Screenplay.Tasks;
+
+public class GetTheItemDisplayedAtAPosition(int position) : IPerformableWithResult, ICanReport
+{
+ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
+ => formatter.Format("{Actor} gets the item displayed at position {Position}", actor, position);
+
+ public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
+ => actor.PerformAsync(ReadFromTheElement(TodoApp.TodoAtPosition(position)).TheText(), cancellationToken);
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/ToggleAnItemsCompletedState.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/ToggleAnItemsCompletedState.cs
new file mode 100644
index 00000000..9e59aa14
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/ToggleAnItemsCompletedState.cs
@@ -0,0 +1,12 @@
+using static CSF.Screenplay.Selenium.PerformableBuilder;
+
+namespace CSF.Screenplay.Tasks;
+
+public class ToggleAnItemsCompletedState(int position) : IPerformable, ICanReport
+{
+ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
+ => formatter.Format("{Actor} toggles the completion state of the item at position {Position}", actor, position);
+
+ public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
+ => actor.PerformAsync(ClickOn(TodoApp.CompletedToggle(position)));
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/TestRunSetup.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/TestRunSetup.cs
new file mode 100644
index 00000000..5fe51489
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/TestRunSetup.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Http;
+using System.Reflection;
+using System.Text;
+using CSF.Extensions.WebDriver.Factories;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Reqnroll.BoDi;
+
+namespace CSF.Screenplay;
+
+///
+/// Configures the Reqnroll test run appropriately to use Screenplay/Selenium, and to ensure that the TodoMVC sample app
+/// is started and running automatically when the test run is performed.
+///
+[Binding]
+public static class TestRunSetup
+{
+ const string
+ testProjectName = "CSF.Screenplay.Samples.SimpleToDoAppTests",
+ sampleAppName = "CSF.Screenplay.Samples.SimpleToDoApp",
+ sampleAppUrl = "http://localhost:5173/";
+
+ internal static readonly Uri SampleAppUri = new(sampleAppUrl);
+
+ static Process? sampleAppProcess;
+ static readonly ConcurrentQueue sampleAppOutput = new();
+
+ [BeforeTestRun]
+ public static async Task BeforeTestRunAsync(IObjectContainer container)
+ {
+ SetupDependencyInjection(container);
+ await StartSampleAppAsync();
+ }
+
+ [AfterTestRun]
+ public static async Task AfterTestRunAsync()
+ {
+ await StopSampleAppAsync();
+ }
+
+ ///
+ /// Gets the working directory to the sample application.
+ ///
+ /// A string directory path
+ static string GetSampleAppWorkingDirectory()
+ {
+ var path = Path.Combine(GetTestProjectDirectory(), "..", sampleAppName);
+ if(!Directory.Exists(path)) throw new DriveNotFoundException($"The sample application directory must exist: {path}");
+ return path;
+ }
+
+ ///
+ /// Gets the file system directory path to the location which contains this test project
+ ///
+ /// A string directory path
+ /// If this assembly is loaded other than from the file system
+ /// If the test project is not an ancestor of the directory which contains this test project
+ static string GetTestProjectDirectory()
+ {
+ var testDirectory = Assembly.GetExecutingAssembly().Location;
+ if(string.IsNullOrWhiteSpace(testDirectory))
+ throw new InvalidOperationException("The current assembly must be loaded from a file system path");
+
+ for(var currentPath = testDirectory; !string.IsNullOrEmpty(currentPath); currentPath = Directory.GetParent(currentPath!)?.FullName)
+ {
+ var directory = new DirectoryInfo(currentPath);
+ if(directory.Exists && directory.Name == testProjectName)
+ return directory.FullName;
+ }
+
+ throw new DirectoryNotFoundException($"The assembly {testDirectory} must be contained within the test project directory: {testProjectName}");
+ }
+
+ ///
+ /// Sets up Reqnroll dependency injection for this test run.
+ ///
+ ///
+ ///
+ /// The Reqnroll Screenplay plugin adds Screenplay to DI automatically, but we must add a few things manually:
+ ///
+ ///
+ /// - The configuration, using Microsoft.Extensions.Configuration. This must be added manually because we are not using
+ /// Generic Host here.
+ /// - The Screenplay/Selenium plugin.
+ /// - An adapter for the MS Options pattern. This is required because the Reqnroll DI container comes with some limitations
+ /// which cannot be worked around automatically. One of these means that it is not compatible with the MS Options pattern without this
+ /// additional DI registration.
+ /// - The Webster persona, which would need registering in any case.
+ ///
+ ///
+ /// The Reqnroll DI container
+ static void SetupDependencyInjection(IObjectContainer container)
+ {
+ container.ToServiceCollection()
+ .AddSingleton(GetConfiguration)
+ .AddSelenium()
+ .AddOptionsAdapters()
+ .AddTransient();
+ }
+
+ static IConfiguration GetConfiguration(IServiceProvider _)
+ {
+ var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
+ return builder.Build();
+ }
+
+ ///
+ /// Creates and starts an OS process which runs NPM, to start up the TodoMVC sample application.
+ ///
+ ///
+ ///
+ /// The OS process handle is stored at , so that it may be stopped gracefully at a later stage.
+ ///
+ ///
+ /// This includes logic which waits for the sample app to come up (via npm run serve) and which attempts to clean up a dangling
+ /// process should there be a failure of some kind.
+ /// Additionally, if we are unable to connect to the sample app, an is thrown, which includes
+ /// the output from NPM. This should help diagnose the problem.
+ ///
+ ///
+ /// An awaitable task
+ static async Task StartSampleAppAsync()
+ {
+ var startInfo = GetSampleAppStartInfo();
+ sampleAppProcess = new Process { StartInfo = startInfo };
+ sampleAppProcess.OutputDataReceived += (_, e) => { if (e.Data is not null) sampleAppOutput.Enqueue($"[stdout] {e.Data}"); };
+ sampleAppProcess.ErrorDataReceived += (_, e) => { if (e.Data is not null) sampleAppOutput.Enqueue($"[stderr] {e.Data}"); };
+
+ try
+ {
+ sampleAppProcess.Start();
+ sampleAppProcess.BeginOutputReadLine();
+ sampleAppProcess.BeginErrorReadLine();
+
+ await WaitForSampleAppAsync(sampleAppProcess);
+ }
+ catch (Exception ex)
+ {
+ try { if (!sampleAppProcess.HasExited) sampleAppProcess.Kill(true); } catch { /* intentional no-op */ }
+
+ throw new InvalidOperationException(
+ $"The sample app did not start correctly, or was unreachable.{Environment.NewLine}" +
+ $"Working directory: {startInfo.WorkingDirectory}{Environment.NewLine}" +
+ $"Command: {startInfo.FileName} {string.Join(' ', startInfo.ArgumentList)}{Environment.NewLine}" +
+ $"--- npm output ---{Environment.NewLine}{GetSampleAppOutput()}{Environment.NewLine}--- end npm output ---",
+ ex);
+ }
+ }
+
+ ///
+ /// Gets the start info for the sample app process.
+ ///
+ ///
+ ///
+ /// Executing NPM via Windows requires cmd.exe, because NPM is a npm.cmd batch-file formatted file.
+ /// On Linux (CI environments) though, it is not.
+ ///
+ ///
+ /// Process start info
+ static ProcessStartInfo GetSampleAppStartInfo()
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ WorkingDirectory = GetSampleAppWorkingDirectory(),
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+
+ if (OperatingSystem.IsWindows())
+ {
+ startInfo.FileName = "cmd.exe";
+ foreach (var arg in new[] { "/c", "npm", "run", "serve" }) startInfo.ArgumentList.Add(arg);
+ }
+ else
+ {
+ startInfo.FileName = "npm";
+ foreach (var arg in new[] { "run", "serve" }) startInfo.ArgumentList.Add(arg);
+ }
+
+ return startInfo;
+ }
+ static string GetSampleAppOutput()
+ {
+ var sb = new StringBuilder();
+ foreach (var line in sampleAppOutput) sb.AppendLine(line);
+ return sb.Length == 0 ? "(no output captured)" : sb.ToString();
+ }
+
+ ///
+ /// Makes up to 10 attempts to connect to the sample application, over a maximum of approx 28 seconds.
+ ///
+ ///
+ ///
+ /// If the NPM process has exited early then this algorithm fails fast with an .
+ /// This method uses a very simple exponential backoff algorithm for the retries, capped at a 5 second wait between retries.
+ /// Over the total of 10 attempts, this puts us at a maximum wait of approx 28 seconds.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ static async Task WaitForSampleAppAsync(Process process)
+ {
+ const int maxConnectionAttempts = 10;
+
+ using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
+ Exception? lastError = null;
+
+ for (var attempt = 1; attempt <= maxConnectionAttempts; attempt++)
+ {
+ if (process.HasExited)
+ {
+ process.WaitForExit();
+ throw new InvalidOperationException($"The npm process exited prematurely with exit code {process.ExitCode}.");
+ }
+
+ try
+ {
+ using var response = await httpClient.GetAsync(sampleAppUrl);
+ response.EnsureSuccessStatusCode();
+ return;
+ }
+ catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
+ {
+ lastError = ex;
+ if (attempt == maxConnectionAttempts) break;
+ var delay = TimeSpan.FromMilliseconds(Math.Min(250 * Math.Pow(2, attempt - 1), 5000));
+ await Task.Delay(delay);
+ }
+ }
+
+ throw new TimeoutException($"The sample app did not respond after {maxConnectionAttempts} attempts.", lastError);
+ }
+
+ ///
+ /// Stops the OS process which runs the TodoMVC sample application.
+ ///
+ /// An awaitable task
+ static async Task StopSampleAppAsync()
+ {
+ sampleAppProcess?.Kill(true);
+ }
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/TodoApp.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/TodoApp.cs
new file mode 100644
index 00000000..bbdd64bd
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/TodoApp.cs
@@ -0,0 +1,42 @@
+using CSF.Screenplay;
+using CSF.Screenplay.Selenium.Elements;
+
+///
+/// Constants which represent URLs and UI elements associated with the Todo app.
+///
+///
+///
+/// In the Screenplay pattern (which is a heavy refactoring of the Page Object Pattern), types like this are all that remains of the Page Object Model.
+/// This class carries runtime constants & functions which correspond mainly to the interactive UI elements which this test suite interacts with.
+/// The only other is a named URI which represents the app's web address (within a base URL, configured in the persona).
+///
+///
+/// Obviously, the TodoMVC app is very small, so just one static class is ample to contain all these values without confusion.
+/// In a more realistic-size test project, there might be many such classes, each representing either a page/screen, or even area/logical section of a screen.
+/// Whatever is suitable to organise such constants/functions, appropriate to the design of the app.
+///
+///
+/// and instances are given human-readable names, so that they are readable (to devs and non-devs alike)
+/// when they show up in reports.
+///
+///
+public static class TodoApp
+{
+ public static NamedUri RootUri = new NamedUri("/", "The root of the todo app");
+
+ public static Locator PageHeading = new CssSelector(".header h1", "The main page heading");
+
+ public static Locator NewItem = new CssSelector(".header .new-todo", "The new-item entry control");
+
+ public static Locator DisplayedTodos = new CssSelector(".main .todo-list li", "The displayed todo items");
+
+ public static Locator TodoAtPosition(int position) => new CssSelector($".main .todo-list li:nth-child({position}) .view label", $"The text of the displayed todo item at position {position}");
+
+ public static Locator CompletedToggle(int position) => new CssSelector($".main .todo-list li:nth-child({position}) .view input", $"The 'completed' toggle for the todo item at position {position}");
+
+ public static Locator ShowAllFilter => new CssSelector(".footer .filters [href=\"#/\"]", "The clear-filter (show all) button");
+
+ public static Locator ActiveOnlyFilter => new CssSelector(".footer .filters [href=\"#/active\"]", "The active-only filter button");
+
+ public static Locator CompletedOnlyFilter => new CssSelector(".footer .filters [href=\"#/completed\"]", "The completed-only filter button");
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Webster.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Webster.cs
new file mode 100644
index 00000000..ae27a0ef
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Webster.cs
@@ -0,0 +1,32 @@
+using System;
+using CSF.Extensions.WebDriver;
+
+namespace CSF.Screenplay;
+
+///
+/// Webster is a Persona who can browse the web.
+///
+///
+///
+/// The has more information about Personas and how to use them.
+///
+///
+/// An web driver factory
+public class Webster(IGetsWebDriver webDriverFactory) : IPersona
+{
+ ///
+ public string Name => nameof(Webster);
+
+ ///
+ public Actor GetActor(Guid performanceIdentity)
+ {
+ var webster = new Actor(Name, performanceIdentity);
+ webster.IsAbleTo(BrowseTheWeb());
+ webster.IsAbleTo(UseABaseUri());
+ return webster;
+ }
+
+ BrowseTheWeb BrowseTheWeb() => new(webDriverFactory);
+
+ static UseABaseUri UseABaseUri() => new(TestRunSetup.SampleAppUri);
+}
\ No newline at end of file
diff --git a/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/appsettings.json b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/appsettings.json
new file mode 100644
index 00000000..c1927be9
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "WebDriverFactory": {
+ "DriverConfigurations": {
+ "CiHeadlessChrome": {
+ "DriverType": "ChromeDriver",
+ "OptionsCustomizerType ": "CSF.Screenplay.CiChromeOptionsCustomizer, CSF.Screenplay.Samples.SimpleToDoAppTests",
+ "BrowserLogLevel": "Info"
+ }
+ },
+ "SelectedConfiguration": "CiHeadlessChrome"
+ }
+}
diff --git a/Samples/README.md b/Samples/README.md
new file mode 100644
index 00000000..b76ccca8
--- /dev/null
+++ b/Samples/README.md
@@ -0,0 +1,7 @@
+# Samples
+
+This directory contains sample projects which consume CSF.Screenplay.
+These could be considered _as part of the documentation_.
+
+* **CSF.Screenplay.Samples.SimpleToDoApp** is a small/simple "To Do List" web app which can be tested by Screenplay tests.
+* **CSF.Screenplay.Samples.SimpleToDoAppTests** is a BDD test suite written using Reqnroll, Selenium and Screenplay to test the simple To Do List app.
diff --git a/Tests/CSF.Screenplay.Reqnroll.Tests/CSF.Screenplay.Reqnroll.Tests.csproj b/Tests/CSF.Screenplay.Reqnroll.Tests/CSF.Screenplay.Reqnroll.Tests.csproj
index a68fbe5b..9c19e01f 100644
--- a/Tests/CSF.Screenplay.Reqnroll.Tests/CSF.Screenplay.Reqnroll.Tests.csproj
+++ b/Tests/CSF.Screenplay.Reqnroll.Tests/CSF.Screenplay.Reqnroll.Tests.csproj
@@ -18,6 +18,7 @@
+
diff --git a/Tests/CSF.Screenplay.Reqnroll.Tests/ServiceProviderAdapterTests.cs b/Tests/CSF.Screenplay.Reqnroll.Tests/ServiceProviderAdapterTests.cs
new file mode 100644
index 00000000..cabe1147
--- /dev/null
+++ b/Tests/CSF.Screenplay.Reqnroll.Tests/ServiceProviderAdapterTests.cs
@@ -0,0 +1,71 @@
+using System;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Reqnroll.BoDi;
+
+namespace CSF.Screenplay
+{
+ [TestFixture, Parallelizable]
+ public class ServiceProviderAdapterTests
+ {
+ [Test]
+ public void GetServiceShouldResolveFromTheWrappedContainer()
+ {
+ var expected = new object();
+ var wrapped = new Mock();
+ wrapped.Setup(x => x.Resolve(typeof(object), null)).Returns(expected);
+ var sut = new ServiceProviderAdapter(wrapped.Object);
+
+ Assert.That(sut.GetService(typeof(object)), Is.SameAs(expected));
+ }
+
+ [Test]
+ public void GetServiceShouldReturnNullIfWrappedContainerThrowsObjectContainerException()
+ {
+ var wrapped = new Mock();
+ var logger = new Mock>();
+ wrapped.Setup(x => x.Resolve(typeof(object), null)).Throws(new ObjectContainerException("Cannot resolve", Array.Empty()));
+ wrapped.Setup(x => x.Resolve>()).Returns(logger.Object);
+ var sut = new ServiceProviderAdapter(wrapped.Object);
+
+ using var scrope = Assert.EnterMultipleScope();
+ Assert.That(() => sut.GetService(typeof(object)), Throws.Nothing, "Does not throw");
+ Assert.That(sut.GetService(typeof(object)), Is.Null, "Returns null");
+ }
+
+ [Test]
+ public void GetServiceShouldGetALoggerIfRegisteredAndTheWrappedContainerThrowsObjectContainerException()
+ {
+ var wrapped = new Mock();
+ var logger = new Mock>();
+ wrapped.Setup(x => x.Resolve(typeof(object), null)).Throws(new ObjectContainerException("Cannot resolve", Array.Empty()));
+ wrapped.Setup(x => x.Resolve>()).Returns(logger.Object);
+ var sut = new ServiceProviderAdapter(wrapped.Object);
+
+ sut.GetService(typeof(object));
+
+ wrapped.Verify(x => x.Resolve>(), Times.Once);
+ }
+
+ [Test]
+ public void GetServiceShouldNotThrowIfGettingALoggerThrowsObjectContainerException()
+ {
+ var wrapped = new Mock();
+ wrapped.Setup(x => x.Resolve(typeof(object), null)).Throws(new ObjectContainerException("Cannot resolve", Array.Empty()));
+ wrapped.Setup(x => x.Resolve>()).Throws(new ObjectContainerException("Cannot resolve", Array.Empty()));
+ var sut = new ServiceProviderAdapter(wrapped.Object);
+
+ Assert.That(() => sut.GetService(typeof(object)), Throws.Nothing);
+ }
+
+ [Test]
+ public void GetServiceShouldNotCatchAnyOtherKindOfException()
+ {
+ var wrapped = new Mock();
+ wrapped.Setup(x => x.Resolve(typeof(object), null)).Throws(new InvalidOperationException());
+ var sut = new ServiceProviderAdapter(wrapped.Object);
+
+ Assert.That(() => sut.GetService(typeof(object)), Throws.InvalidOperationException);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/CSF.Screenplay.Tests/Performables/CopyFileAsAnAssetTests.cs b/Tests/CSF.Screenplay.Tests/Performables/CopyFileAsAnAssetTests.cs
index c728fdb0..5cf645d2 100644
--- a/Tests/CSF.Screenplay.Tests/Performables/CopyFileAsAnAssetTests.cs
+++ b/Tests/CSF.Screenplay.Tests/Performables/CopyFileAsAnAssetTests.cs
@@ -2,7 +2,7 @@
using CSF.Screenplay.Abilities;
using CSF.Screenplay.Reporting;
using NUnit.Framework.Legacy;
-using static CSF.Screenplay.PerforamableBuilder;
+using static CSF.Screenplay.PerformableBuilder;
namespace CSF.Screenplay.Performables;
diff --git a/Tests/CSF.Screenplay.Tests/Performables/SaveAStreamAsAnAssetTests.cs b/Tests/CSF.Screenplay.Tests/Performables/SaveAStreamAsAnAssetTests.cs
index c01262e7..6f4b09da 100644
--- a/Tests/CSF.Screenplay.Tests/Performables/SaveAStreamAsAnAssetTests.cs
+++ b/Tests/CSF.Screenplay.Tests/Performables/SaveAStreamAsAnAssetTests.cs
@@ -2,7 +2,7 @@
using CSF.Screenplay.Abilities;
using CSF.Screenplay.Reporting;
using NUnit.Framework.Legacy;
-using static CSF.Screenplay.PerforamableBuilder;
+using static CSF.Screenplay.PerformableBuilder;
namespace CSF.Screenplay.Performables;
diff --git a/Tests/CSF.Screenplay.Tests/Performables/SaveTextAsAnAssetTests.cs b/Tests/CSF.Screenplay.Tests/Performables/SaveTextAsAnAssetTests.cs
index f76e170b..f419e2aa 100644
--- a/Tests/CSF.Screenplay.Tests/Performables/SaveTextAsAnAssetTests.cs
+++ b/Tests/CSF.Screenplay.Tests/Performables/SaveTextAsAnAssetTests.cs
@@ -2,7 +2,7 @@
using CSF.Screenplay.Abilities;
using CSF.Screenplay.Reporting;
using NUnit.Framework.Legacy;
-using static CSF.Screenplay.PerforamableBuilder;
+using static CSF.Screenplay.PerformableBuilder;
namespace CSF.Screenplay.Performables;
diff --git a/Tests/CSF.Screenplay.Tests/Performables/StopwatchTests.cs b/Tests/CSF.Screenplay.Tests/Performables/StopwatchTests.cs
index fe5a85e8..d5e06a0e 100644
--- a/Tests/CSF.Screenplay.Tests/Performables/StopwatchTests.cs
+++ b/Tests/CSF.Screenplay.Tests/Performables/StopwatchTests.cs
@@ -1,5 +1,5 @@
using CSF.Screenplay.Abilities;
-using static CSF.Screenplay.PerforamableBuilder;
+using static CSF.Screenplay.PerformableBuilder;
using static CSF.Screenplay.PerformanceStarter;
namespace CSF.Screenplay.Performables;