Skip to content

Commit

Permalink
Add a new event for actors to record assets
Browse files Browse the repository at this point in the history
This allows files which are generated by performables to be
recorded along with the Screenplay report.
For example, things like screenshots or logs or other generated
file data.
  • Loading branch information
craigfowler committed Oct 1, 2024
1 parent ec7788b commit 81e1350
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 32 deletions.
17 changes: 17 additions & 0 deletions CSF.Screenplay.Abstractions/Actor.events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,22 @@ protected virtual void InvokeGainedAbility(object ability)
var args = new GainAbilityEventArgs(Name, PerformanceIdentity, ability);
GainedAbility?.Invoke(this, args);
}

/// <inheritdoc/>
public event EventHandler<PerformableAssetEventArgs> RecordsAsset;

/// <summary>
/// Invokes the <see cref="RecordsAsset"/> event.
/// </summary>
/// <param name="performable">The performable item</param>
/// <param name="filePath">The full absolute path to the asset file</param>
/// <param name="fileSummary">An optional human-readable summary of the asset file</param>
/// <param name="phase">The performance phase to which this event relates</param>
protected virtual void InvokeRecordsAsset(object performable, string filePath, string fileSummary = null, PerformancePhase phase = PerformancePhase.Unspecified)
{
var args = new PerformableAssetEventArgs(Name, PerformanceIdentity, performable, filePath, fileSummary, phase);
RecordsAsset?.Invoke(this, args);
}

}
}
56 changes: 26 additions & 30 deletions CSF.Screenplay.Abstractions/Actor.performer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,54 +37,55 @@ protected virtual async ValueTask PerformAsync(IPerformable performable,
}

/// <summary>
/// Performs an action or task which returns an untyped result.
/// Performs a question or question-like task which returns an untyped result.
/// </summary>
/// <param name="performable">The performable item</param>
/// <param name="cancellationToken">An optional token to cancel the performable</param>
/// <param name="phase">The performance phase to which the performable belongs</param>
/// <returns>A task which completes when the performable is complete</returns>
/// <exception cref="ArgumentNullException">If the performable is <see langword="null" /></exception>
protected virtual async ValueTask<object> PerformAsync(IPerformableWithResult performable,
PerformancePhase phase = PerformancePhase.Unspecified,
CancellationToken cancellationToken = default)
protected virtual ValueTask<object> PerformAsync(IPerformableWithResult performable,
PerformancePhase phase = PerformancePhase.Unspecified,
CancellationToken cancellationToken = default)
{
if(performable is null) throw new ArgumentNullException(nameof(performable));
AssertNotDisposed();

try
{
InvokeBeginPerformable(performable, phase);
var result = await performable.PerformAsAsync(this, cancellationToken).ConfigureAwait(false);
InvokePerformableResult(performable, result);
InvokeEndPerformable(performable, phase);
return result;
}
catch(Exception ex)
{
InvokePerformableFailed(performable, ex, phase);
throw GetPerformableException(performable, ex);
}
return PerformAsync(performable, phase, () => performable.PerformAsAsync(this, cancellationToken));
}

/// <summary>
/// Performs an action or task which returns a strongly typed result.
/// Performs a question or question-like task which returns a strongly typed result.
/// </summary>
/// <param name="performable">The performable item</param>
/// <param name="cancellationToken">An optional token to cancel the performable</param>
/// <param name="phase">The performance phase to which the performable belongs</param>
/// <returns>A task which completes when the performable is complete</returns>
/// <exception cref="ArgumentNullException">If the performable is <see langword="null" /></exception>
protected virtual async ValueTask<T> PerformAsync<T>(IPerformableWithResult<T> performable,
PerformancePhase phase = PerformancePhase.Unspecified,
CancellationToken cancellationToken = default)
protected virtual ValueTask<T> PerformAsync<T>(IPerformableWithResult<T> performable,
PerformancePhase phase = PerformancePhase.Unspecified,
CancellationToken cancellationToken = default)
{
if(performable is null) throw new ArgumentNullException(nameof(performable));
AssertNotDisposed();

return PerformAsync(performable, phase, () => performable.PerformAsAsync(this, cancellationToken));
}

PerformableException GetPerformableException(object performable, Exception ex)
{
return new PerformableException($"{Name} encountered an unexpected exception whilst performing {DefaultStrings.FormatValue(performable)}", ex)
{
Performable = performable,
};
}

async ValueTask<T> PerformAsync<T>(object performable, PerformancePhase phase, Func<ValueTask<T>> performableFunc)
{
try
{
InvokeBeginPerformable(performable, phase);
var result = await performable.PerformAsAsync(this, cancellationToken).ConfigureAwait(false);
var result = await performableFunc().ConfigureAwait(false);
InvokePerformableResult(performable, result);
InvokeEndPerformable(performable, phase);
return result;
Expand All @@ -96,14 +97,6 @@ protected virtual async ValueTask<T> PerformAsync<T>(IPerformableWithResult<T> p
}
}

PerformableException GetPerformableException(object performable, Exception ex)
{
return new PerformableException($"{Name} encountered an unexpected exception whilst performing {DefaultStrings.FormatValue(performable)}", ex)
{
Performable = performable,
};
}

ValueTask ICanPerform.PerformAsync(IPerformable performable, CancellationToken cancellationToken)
=> PerformAsync(performable, cancellationToken: cancellationToken);

Expand All @@ -112,5 +105,8 @@ ValueTask<object> ICanPerform.PerformAsync(IPerformableWithResult performable, C

ValueTask<T> ICanPerform.PerformAsync<T>(IPerformableWithResult<T> performable, CancellationToken cancellationToken)
=> PerformAsync(performable, cancellationToken: cancellationToken);

void ICanPerform.RecordAsset(object performable, string filePath, string fileSummary)
=> InvokeRecordsAsset(performable, filePath, fileSummary);
}
}
5 changes: 5 additions & 0 deletions CSF.Screenplay.Abstractions/Actors/IHasPerformableEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,10 @@ public interface IHasPerformableEvents
/// Occurs when an actor gains a new ability.
/// </summary>
event EventHandler<GainAbilityEventArgs> GainedAbility;

/// <summary>
/// Occurs when an actor records the presence of a new file asset.
/// </summary>
event EventHandler<PerformableAssetEventArgs> RecordsAsset;
}
}
42 changes: 42 additions & 0 deletions CSF.Screenplay.Abstractions/Actors/PerformableAssetEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;

namespace CSF.Screenplay.Actors
{
/// <summary>
/// Event arguments which represent the revealing of a file asset which relates to a performable.
/// </summary>
/// <seealso cref="ICanPerform.RecordAsset(object, string, string)"/>
public class PerformableAssetEventArgs : PerformableEventArgs
{
/// <summary>
/// Gets a full/absolute path to the asset file.
/// </summary>
public string FilePath { get; }

/// <summary>
/// Gets an optional human-readable summary of what this asset represents. This should be one sentence at most, suitable
/// for display in a UI tool-tip.
/// </summary>
public string FileSummary { get; }

/// <summary>
/// Initializes a new instance of <see cref="PerformableAssetEventArgs"/>.
/// </summary>
/// <param name="actorName">The actor's name</param>
/// <param name="performanceIdentity">The actor's performance identity</param>
/// <param name="performable">The performable item</param>
/// <param name="filePath">The full absolute path to the asset file</param>
/// <param name="fileSummary">An optional human-readable summary of the asset file</param>
/// <param name="phase">The phase of performance</param>
public PerformableAssetEventArgs(string actorName,
Guid performanceIdentity,
object performable,
string filePath,
string fileSummary = null,
PerformancePhase phase = PerformancePhase.Unspecified) : base(actorName, performanceIdentity, performable, phase)
{
FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
FileSummary = fileSummary;
}
}
}
29 changes: 27 additions & 2 deletions CSF.Screenplay.Abstractions/ICanPerform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,45 @@ public interface ICanPerform
ValueTask PerformAsync(IPerformable performable, CancellationToken cancellationToken = default);

/// <summary>
/// Performs an action or task which returns an untyped result.
/// Performs a question or question-like task which returns an untyped result.
/// </summary>
/// <param name="performable">The performable item</param>
/// <param name="cancellationToken">An optional token to cancel the performable</param>
/// <returns>A task which exposes a result when the performable is complete</returns>
ValueTask<object> PerformAsync(IPerformableWithResult performable, CancellationToken cancellationToken = default);

/// <summary>
/// Performs an action or task which returns a strongly typed result.
/// Performs a question or question-like task which returns a strongly typed result.
/// </summary>
/// <param name="performable">The performable item</param>
/// <param name="cancellationToken">An optional token to cancel the performable</param>
/// <typeparam name="T">The result type</typeparam>
/// <returns>A task which exposes a result when the performable is complete</returns>
ValueTask<T> PerformAsync<T>(IPerformableWithResult<T> performable, CancellationToken cancellationToken = default);

/// <summary>
/// Records the existence of a new performable asset file at the specified path.
/// </summary>
/// <remarks>
/// <para>
/// File assets are sometimes created during a performance as a reporting/verification mechanism. For example a performable
/// which controls the user interface of an application might take and save a screenshot of that UI so that a human may later
/// verify that everything looked as it should.
/// </para>
/// <para>
/// Alternatively, file assets might constitute part of the output of a performance. Imagine an application of Screenplay which
/// captures video from a security camera; that video file would be an asset.
/// </para>
/// <para>
/// This method may be used from performables which generate and save asset files.
/// They ensure that appropriate events are called and passed 'upward' through the Screenplay architecture, such that
/// subscribers may be notified.
/// This will allow the presence and details of assets to be included in Screenplay artifacts such as reports.
/// </para>
/// </remarks>
/// <param name="performable">The performable item</param>
/// <param name="filePath">The full absolute path to the asset file</param>
/// <param name="fileSummary">An optional human-readable summary of the asset file</param>
void RecordAsset(object performable, string filePath, string fileSummary = null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ public interface IHasPerformanceEvents
/// </summary>
event EventHandler<PerformableFailureEventArgs> PerformableFailed;

/// <summary>
/// Occurs when an actor records the presence of a new file asset.
/// </summary>
event EventHandler<PerformableAssetEventArgs> RecordsAsset;

#endregion

#region Actors
Expand Down
8 changes: 8 additions & 0 deletions CSF.Screenplay/Performances/PerformanceEventBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ void OnPerformableResult(object sender, PerformableResultEventArgs args)
void OnPerformableFailed(object sender, PerformableFailureEventArgs args)
=> PerformableFailed?.Invoke(sender, args);

/// <inheritdoc/>
public event EventHandler<PerformableAssetEventArgs> RecordsAsset;

void OnRecordsAsset(object sender, PerformableAssetEventArgs args)
=> RecordsAsset?.Invoke(sender, args);

#endregion

#region Pub: Actors
Expand Down Expand Up @@ -114,6 +120,7 @@ public void SubscribeTo(Actor actor)
actor.PerformableResult += OnPerformableResult;
actor.PerformableFailed += OnPerformableFailed;
actor.GainedAbility += OnGainedAbility;
actor.RecordsAsset += OnRecordsAsset;
}

/// <inheritdoc/>
Expand All @@ -127,6 +134,7 @@ public void UnsubscribeFrom(Actor actor)
actor.PerformableResult -= OnPerformableResult;
actor.PerformableFailed -= OnPerformableFailed;
actor.GainedAbility -= OnGainedAbility;
actor.RecordsAsset -= OnRecordsAsset;

if(!subscribedActors.TryGetValue(((IHasPerformanceIdentity)actor).PerformanceIdentity, out var actorsForPerformance)) return;
actorsForPerformance.Remove(actor);
Expand Down
31 changes: 31 additions & 0 deletions Tests/CSF.Screenplay.Tests/Integration/EventBusIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,35 @@ public void CompleteScreenplayShouldEmitTheCorrectEvent([DefaultScreenplay] Scre

Assert.That(ended, Is.True);
}

[Test,AutoMoqData]
public async Task RecordAssetShouldEmitTheCorrectEvent([DefaultScreenplay] Screenplay sut, string expectedPath, string expectedSummary, object performable)
{
string? filePath = null, fileSummary = null;
void OnRecordsAsset(object? sender, PerformableAssetEventArgs e)
{
filePath = e.FilePath;
fileSummary = e.FileSummary;
};

var eventPublisher = sut.ServiceProvider.GetRequiredService<IHasPerformanceEvents>();
eventPublisher.RecordsAsset += OnRecordsAsset;

await sut.ExecuteAsPerformanceAsync((s, c) =>
{
var cast = s.GetRequiredService<ICast>();
var joe = cast.GetActor("Joe");
joe.RecordAsset(performable, expectedPath, expectedSummary);

return Task.FromResult<bool?>(true);
});

eventPublisher.RecordsAsset -= OnRecordsAsset;

Assert.Multiple(() =>
{
Assert.That(filePath, Is.EqualTo(expectedPath), "File path");
Assert.That(fileSummary, Is.EqualTo(expectedSummary), "File summary");
});
}
}

0 comments on commit 81e1350

Please sign in to comment.