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(),