diff --git a/CSF.Screenplay.Abstractions/IHasCustomTypeName.cs b/CSF.Screenplay.Abstractions/IHasCustomTypeName.cs
new file mode 100644
index 00000000..298fc771
--- /dev/null
+++ b/CSF.Screenplay.Abstractions/IHasCustomTypeName.cs
@@ -0,0 +1,30 @@
+namespace CSF.Screenplay
+{
+ ///
+ /// An object which can provide a custom human-readable .NET type name.
+ ///
+ ///
+ ///
+ /// This is particularly important when reporting, particularly when writing the name of the performable type
+ /// into a report. Some performables may be written using the adapter or decorator patterns, in which a general-use
+ /// class wraps a specific class which implements a subset of a performable. Using
+ /// will yield the type name of the general-use 'outer' class, which is usually not very useful on its own.
+ ///
+ ///
+ /// If general-use performables, such as adapters, implement this interface, then they can return more useful human-readable
+ /// type names to the consuming logic, making use of their inner/wrapped implementation type.
+ ///
+ ///
+ public interface IHasCustomTypeName
+ {
+ ///
+ /// Gets a human-readable name of the type of the current instance.
+ ///
+ ///
+ /// See the remarks on ; this does not need to be the same as .
+ ///
+ /// A human-readable name of the .NET type of the current instance, which could (for example) be
+ /// qualified with additional context, such as a wrapped implementation.
+ string GetHumanReadableTypeName();
+ }
+}
\ No newline at end of file
diff --git a/CSF.Screenplay.Selenium/Actions/SingleElementPerformableAdapter.cs b/CSF.Screenplay.Selenium/Actions/SingleElementPerformableAdapter.cs
index e1ad2c3f..fe6913fe 100644
--- a/CSF.Screenplay.Selenium/Actions/SingleElementPerformableAdapter.cs
+++ b/CSF.Screenplay.Selenium/Actions/SingleElementPerformableAdapter.cs
@@ -18,7 +18,7 @@ namespace CSF.Screenplay.Selenium.Actions
/// to fetch it using the WebDriver more than once. As such, instances of this adapter (like all performables) should not be re-used.
///
///
- public class SingleElementPerformableAdapter : IPerformable, ICanReport
+ public class SingleElementPerformableAdapter : IPerformable, ICanReport, IHasCustomTypeName
{
readonly ISingleElementPerformable performable;
readonly ITarget target;
@@ -38,6 +38,10 @@ public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellatio
return performable.PerformAsAsync(actor, actor.GetAbility().WebDriver, lazyElement, cancellationToken);
}
+ ///
+ public string GetHumanReadableTypeName()
+ => $"{performable.GetType().FullName}, via {nameof(SingleElementPerformableAdapter)}";
+
///
/// Initializes a new instance of the class with the specified performable and target.
///
diff --git a/CSF.Screenplay.Selenium/Questions/ElementCollectionPerformableWithResultAdapter.cs b/CSF.Screenplay.Selenium/Questions/ElementCollectionPerformableWithResultAdapter.cs
index 020157f1..847489ca 100644
--- a/CSF.Screenplay.Selenium/Questions/ElementCollectionPerformableWithResultAdapter.cs
+++ b/CSF.Screenplay.Selenium/Questions/ElementCollectionPerformableWithResultAdapter.cs
@@ -19,7 +19,7 @@ namespace CSF.Screenplay.Selenium.Questions
/// to fetch it using the WebDriver more than once. As such, instances of this adapter (like all performables) should not be re-used.
///
///
- public class ElementCollectionPerformableWithResultAdapter : IPerformableWithResult>, ICanReport
+ public class ElementCollectionPerformableWithResultAdapter : IPerformableWithResult>, ICanReport, IHasCustomTypeName
{
readonly IElementCollectionPerformableWithResult performable;
readonly ITarget target;
@@ -39,6 +39,10 @@ public ValueTask> PerformAsAsync(ICanPerform actor, Cance
return performable.PerformAsAsync(actor, actor.GetAbility().WebDriver, lazyElements, cancellationToken);
}
+ ///
+ public string GetHumanReadableTypeName()
+ => $"{performable.GetHumanReadableTypeName()}, via {nameof(ElementCollectionPerformableWithResultAdapter)}";
+
///
/// Initializes a new instance of the class with the specified performable and target.
///
diff --git a/CSF.Screenplay.Selenium/Questions/ElementCollectionQuery.cs b/CSF.Screenplay.Selenium/Questions/ElementCollectionQuery.cs
index cd30c234..f6581e65 100644
--- a/CSF.Screenplay.Selenium/Questions/ElementCollectionQuery.cs
+++ b/CSF.Screenplay.Selenium/Questions/ElementCollectionQuery.cs
@@ -57,6 +57,10 @@ public ReportFragment GetReportFragment(Actor actor, Lazy> PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy elements, CancellationToken cancellationToken = default)
=> new ValueTask>(elements.Value.Select(query.GetValue).ToList());
+ ///
+ public string GetHumanReadableTypeName()
+ => $"{query.GetType().FullName}, via {nameof(ElementCollectionQuery)}";
+
///
/// Initializes a new instance of the class with the specified query.
///
diff --git a/CSF.Screenplay.Selenium/Questions/FindElement.cs b/CSF.Screenplay.Selenium/Questions/FindElement.cs
index 1498d1ac..f1b3a908 100644
--- a/CSF.Screenplay.Selenium/Questions/FindElement.cs
+++ b/CSF.Screenplay.Selenium/Questions/FindElement.cs
@@ -85,6 +85,9 @@ Lazy GetLazyElement(ICanPerform actor)
return new Lazy(() => searchContext);
}
+ ///
+ public string GetHumanReadableTypeName() => GetType().FullName;
+
///
/// Initializes a new instance of the class.
///
diff --git a/CSF.Screenplay.Selenium/Questions/FindElements.cs b/CSF.Screenplay.Selenium/Questions/FindElements.cs
index 960d75f2..0d52e7a7 100644
--- a/CSF.Screenplay.Selenium/Questions/FindElements.cs
+++ b/CSF.Screenplay.Selenium/Questions/FindElements.cs
@@ -84,6 +84,9 @@ Lazy GetLazyElement(ICanPerform actor)
return new Lazy(() => searchContext);
}
+ ///
+ public string GetHumanReadableTypeName() => GetType().FullName;
+
///
/// Initializes a new instance of the class.
///
diff --git a/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs b/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs
index 37d89bc4..6b126fd3 100644
--- a/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs
+++ b/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs
@@ -34,6 +34,9 @@ public class GetShadowRootNatively : ISingleElementPerformableWithResult element, IFormatsReportFragment formatter)
=> formatter.Format("{Actor} gets the Shadow Root node from {Element} using the native Selenium technique", actor, element.Value);
+ ///
+ public string GetHumanReadableTypeName() => GetType().FullName;
+
///
public ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default)
{
diff --git a/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs b/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs
index a23d87d4..6979c0e6 100644
--- a/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs
+++ b/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs
@@ -35,6 +35,9 @@ public class GetShadowRootWithJavaScript : ISingleElementPerformableWithResult element, IFormatsReportFragment formatter)
=> formatter.Format("{Actor} gets the Shadow Root node from {Element} using JavaScript", actor, element.Value);
+ ///
+ public string GetHumanReadableTypeName() => GetType().FullName;
+
///
public async ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default)
{
diff --git a/CSF.Screenplay.Selenium/Questions/IElementCollectionPerformableWithResult.cs b/CSF.Screenplay.Selenium/Questions/IElementCollectionPerformableWithResult.cs
index 7f30427a..3dc306f4 100644
--- a/CSF.Screenplay.Selenium/Questions/IElementCollectionPerformableWithResult.cs
+++ b/CSF.Screenplay.Selenium/Questions/IElementCollectionPerformableWithResult.cs
@@ -21,7 +21,7 @@ namespace CSF.Screenplay.Selenium.Questions
/// .
///
///
- public interface IElementCollectionPerformableWithResult : ICanReportForElements
+ public interface IElementCollectionPerformableWithResult : ICanReportForElements, IHasCustomTypeName
{
///
/// Counterpart to except that this method also offers
diff --git a/CSF.Screenplay.Selenium/Questions/ISingleElementPerformableWithResult.cs b/CSF.Screenplay.Selenium/Questions/ISingleElementPerformableWithResult.cs
index 9a5cd427..6588ec6c 100644
--- a/CSF.Screenplay.Selenium/Questions/ISingleElementPerformableWithResult.cs
+++ b/CSF.Screenplay.Selenium/Questions/ISingleElementPerformableWithResult.cs
@@ -20,7 +20,7 @@ namespace CSF.Screenplay.Selenium.Questions
/// .
///
///
- public interface ISingleElementPerformableWithResult : ICanReportForElement
+ public interface ISingleElementPerformableWithResult : ICanReportForElement, IHasCustomTypeName
{
///
/// Counterpart to except that this method also offers
diff --git a/CSF.Screenplay.Selenium/Questions/SingleElementPerformableWithResultAdapter.cs b/CSF.Screenplay.Selenium/Questions/SingleElementPerformableWithResultAdapter.cs
index 250c731d..6925241c 100644
--- a/CSF.Screenplay.Selenium/Questions/SingleElementPerformableWithResultAdapter.cs
+++ b/CSF.Screenplay.Selenium/Questions/SingleElementPerformableWithResultAdapter.cs
@@ -18,7 +18,7 @@ namespace CSF.Screenplay.Selenium.Questions
/// to fetch it using the WebDriver more than once. As such, instances of this adapter (like all performables) should not be re-used.
///
///
- public class SingleElementPerformableWithResultAdapter : IPerformableWithResult, ICanReport
+ public class SingleElementPerformableWithResultAdapter : IPerformableWithResult, ICanReport, IHasCustomTypeName
{
readonly ISingleElementPerformableWithResult performable;
readonly ITarget target;
@@ -46,6 +46,10 @@ public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken ca
}
}
+ ///
+ public string GetHumanReadableTypeName()
+ => $"{performable.GetHumanReadableTypeName()}, via {nameof(SingleElementPerformableWithResultAdapter)}";
+
///
/// Initializes a new instance of the class with the specified performable and target.
///
diff --git a/CSF.Screenplay.Selenium/Questions/SingleElementQuery.cs b/CSF.Screenplay.Selenium/Questions/SingleElementQuery.cs
index 0ae9372a..09bad6f9 100644
--- a/CSF.Screenplay.Selenium/Questions/SingleElementQuery.cs
+++ b/CSF.Screenplay.Selenium/Questions/SingleElementQuery.cs
@@ -55,6 +55,10 @@ public ReportFragment GetReportFragment(Actor actor, Lazy eleme
public ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default)
=> new ValueTask(query.GetValue(element.Value));
+ ///
+ public string GetHumanReadableTypeName()
+ => $"{query.GetType().FullName}, via {nameof(SingleElementQuery)}";
+
///
/// Initializes a new instance of the class with the specified query.
///
diff --git a/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs b/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs
index c7238e8b..f9c5d77c 100644
--- a/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs
+++ b/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs
@@ -174,7 +174,9 @@ public void BeginPerformable(object performable, Actor actor, string performance
{
var performableReport = new PerformableReport
{
- PerformableType = performable.GetType().FullName,
+ PerformableType = performable is IHasCustomTypeName customName
+ ? customName.GetHumanReadableTypeName()
+ : performable.GetType().FullName,
ActorName = actor.Name,
PerformancePhase = performancePhase,
Started = reportTimer.GetCurrentTime(),