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/.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.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..d580e05b
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CiChromeOptionsCustomizer.cs
@@ -0,0 +1,12 @@
+using CSF.Extensions.WebDriver.Factories;
+using OpenQA.Selenium.Chrome;
+
+namespace CSF.Screenplay;
+
+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..004f2f43
--- /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.
+
+Scenario: A user can create a single todo and it appears in the list
+ Given Webster has opened the todo app
+ 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
+ Given Webster has opened the todo app
+ 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
+ Given Webster has opened the todo app
+ 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..9683293a
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/CreateTodos.feature.cs
@@ -0,0 +1,188 @@
+// ------------------------------------------------------------------------------
+//
+// 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();
+ }
+
+ [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 6
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 7
+ await testRunner.GivenAsync("Webster has opened the todo app", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given ");
+#line hidden
+#line 8
+ await testRunner.WhenAsync("he creates a todo item reading feed the cat", ((string)(null)), ((global::Reqnroll.Table)(null)), "When ");
+#line hidden
+#line 9
+ 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 11
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 12
+ await testRunner.GivenAsync("Webster has opened the todo app", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given ");
+#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 13
+ await testRunner.WhenAsync("he creates several todo items", ((string)(null)), table1, "When ");
+#line hidden
+#line 18
+ 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 20
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 21
+ await testRunner.GivenAsync("Webster has opened the todo app", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given ");
+#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/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..aa85a083
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/PerformableBuilder.cs
@@ -0,0 +1,16 @@
+namespace CSF.Screenplay;
+
+using System.Net;
+using CSF.Screenplay.Tasks;
+using static CSF.Screenplay.Selenium.PerformableBuilder;
+
+public static class PerformableBuilder
+{
+ 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);
+}
\ 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..a59a699e
--- /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.PerformableBuilder;
+
+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/TodoSteps.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/TodoSteps.cs
new file mode 100644
index 00000000..d5da9334
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/StepDefinitions/TodoSteps.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using static CSF.Screenplay.PerformanceStarter;
+using static CSF.Screenplay.PerformableBuilder;
+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));
+ }
+
+ [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));
+ }
+}
\ 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..9cec625b
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Tasks/CreateATodoItem.cs
@@ -0,0 +1,12 @@
+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/TestRunSetup.cs b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/TestRunSetup.cs
new file mode 100644
index 00000000..346f8c4c
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/TestRunSetup.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Http;
+using System.Reflection;
+using CSF.Extensions.WebDriver.Factories;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Reqnroll.BoDi;
+
+namespace CSF.Screenplay;
+
+[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;
+
+ [BeforeTestRun]
+ public static async Task BeforeTestRunAsync(IObjectContainer container)
+ {
+ SetupDependencyInjection(container);
+ await StartSampleAppAsync();
+ }
+
+ [AfterTestRun]
+ public static async Task AfterTestRunAsync()
+ {
+ await StopSampleAppAsync();
+ }
+
+ static string GetSampleAppWorkingDirectory() => Path.Combine(GetTestProjectDirectory(), "..", sampleAppName);
+
+ 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}");
+ }
+
+ 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();
+ }
+
+ static async Task StartSampleAppAsync()
+ {
+ var startInfo = new ProcessStartInfo("npm", ["run", "serve"]) { WorkingDirectory = GetSampleAppWorkingDirectory(), UseShellExecute = true, CreateNoWindow = true };
+ sampleAppProcess = Process.Start(startInfo);
+ await Task.Delay(100);
+
+ using var httpClient = new HttpClient();
+ var result = await httpClient.GetAsync(sampleAppUrl);
+ result.EnsureSuccessStatusCode();
+ }
+
+ static async Task StopSampleAppAsync()
+ {
+ await Task.Delay(100);
+ 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..8fca9c80
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/TodoApp.cs
@@ -0,0 +1,14 @@
+using CSF.Screenplay.Selenium.Elements;
+
+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}");
+}
\ 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..2a2e9e5b
--- /dev/null
+++ b/Samples/CSF.Screenplay.Samples.SimpleToDoAppTests/Webster.cs
@@ -0,0 +1,21 @@
+using System;
+using CSF.Extensions.WebDriver;
+
+namespace CSF.Screenplay;
+
+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