Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/crossBrowserTesting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: Screenplay JSON reports ${{ matrix.browserName }}_${{ matrix.browserVersion }}_${{ matrix.os }}_${{ matrix.osVersion }}
path: Tests/CSF.Screenplay.Selenium.Tests/**/ScreenplayReport_*.json
path: Tests/CSF.Screenplay.Selenium.Tests/**/ScreenplayReport.json
- name: Convert Screenplay reports to HTML
continue-on-error: true
run: |
for report in $(find Tests/CSF.Screenplay.Selenium.Tests/ -type f -name "ScreenplayReport_*.json")
for report in $(find Tests/CSF.Screenplay.Selenium.Tests/ -type f -name "ScreenplayReport.json")
do
reportDir=$(dirname "$report")
outputFile="$reportDir/ScreenplayReport.html"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dotnetCi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: Screenplay JSON reports
path: Tests/**/ScreenplayReport_*.json
path: Tests/**/ScreenplayReport.json
- name: Convert Screenplay reports to HTML
continue-on-error: true
run: |
for report in $(find Tests/ -type f -name "ScreenplayReport_*.json")
for report in $(find Tests/ -type f -name "ScreenplayReport.json")
do
reportDir=$(dirname "$report")
outputFile="$reportDir/ScreenplayReport.html"
Expand Down
2 changes: 1 addition & 1 deletion CSF.Screenplay.Abstractions/Reporting/IGetsReportPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace CSF.Screenplay.Reporting
public interface IGetsReportPath
{
/// <summary>
/// Gets the path to which the report should be written.
/// Gets the directory path to which the report files should be written.
/// </summary>
/// <remarks>
/// <para>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.IO;

namespace CSF.Screenplay.Reporting
{
/// <summary>
/// Extension methods for <see cref="IGetsReportPath"/>.
/// </summary>
public static class ReportPathProviderExtensions
{
const string reportFilename = "ScreenplayReport.json";

/// <summary>
/// Gets the file path to which the report JSON file should be written.
/// </summary>
/// <remarks>
/// <para>
/// If the returned path is <see langword="null" /> then Screenplay's reporting functionality should be disabled and no report should be written.
/// Otherwise, implementations of this interface should return an absolute file system path to which the report JSON file should be written.
/// This path must be writable by the executing process.
/// </para>
/// <para>
/// Reporting could be disabled if either the Screenplay Options report path is <see langword="null" /> or a whitespace-only string, or if the path
/// indicated by those options is not writable.
/// </para>
/// </remarks>
/// <returns>The report file path.</returns>
public static string GetReportFilePath(this IGetsReportPath provider)
{
var directoryPath = provider.GetReportPath();
if(directoryPath is null) return null;
return Path.Combine(directoryPath, reportFilename);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ module.exports = {
]
},
optimization: {
minimizer: [
`...`,
new CssMinimizerPlugin()
]
minimizer: [new CssMinimizerPlugin(), '...']
},
plugins: [
new HtmlWebpackPlugin({
Expand Down
98 changes: 98 additions & 0 deletions CSF.Screenplay.JsonToHtmlReport/AssetEmbedder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CSF.Screenplay.ReportModel;
using System.Linq;
using System.IO;

namespace CSF.Screenplay.JsonToHtmlReport
{
/// <summary>
/// Default implementation of <see cref="IEmbedsReportAssets"/>.
/// </summary>
public class AssetEmbedder : IEmbedsReportAssets
{
/// <inheritdoc/>
public async Task EmbedReportAssetsAsync(ScreenplayReport report, ReportConverterOptions options)
{
if (report is null)
throw new ArgumentNullException(nameof(report));
if (options is null)
throw new ArgumentNullException(nameof(options));

var applicableExtensions = options.GetEmbeddedFileExtensions();
var ignoreExtension = options.ShouldEmbedAllFileTypes();
var maxSizeKb = options.EmbeddedFileSizeThresholdKb;

var allAssets = GetAssets(report);
foreach(var asset in allAssets)
await EmbedAssetIfApplicable(asset, maxSizeKb, applicableExtensions, ignoreExtension);
}

static IEnumerable<PerformableAsset> GetAssets(ScreenplayReport report)
{
return from performance in report.Performances
from performable in GetAllPerformables(performance)
from asset in performable.Assets
select asset;
}

static IEnumerable<PerformableReport> GetAllPerformables(PerformanceReport performance)
{
var open = new List<PerformableReport>();
var closed = new List<PerformableReport>();
open.AddRange(performance.Reportables.OfType<PerformableReport>());

while(open.Count > 0)
{
var current = open.First();
open.RemoveAt(0);
closed.Add(current);
open.AddRange(current.Reportables.OfType<PerformableReport>());
}

return closed;
}

static Task EmbedAssetIfApplicable(PerformableAsset asset,
int maxSizeKb,
IReadOnlyCollection<string> applicableExtensions,
bool ignoreExtension)
{
if(!ShouldEmbed(asset, maxSizeKb, applicableExtensions, ignoreExtension)) return Task.CompletedTask;
return EmbedAssetIfApplicableAsync(asset);
}

static bool ShouldEmbed(PerformableAsset asset,
int maxSizeKb,
IReadOnlyCollection<string> applicableExtensions,
bool ignoreExtension)
{
if(string.IsNullOrWhiteSpace(asset.FilePath)) return false;

var info = new FileInfo(asset.FilePath);
var fileSizeBytes = info.Length;
var extension = info.Extension;

return fileSizeBytes <= (maxSizeKb * 1000)
&& (ignoreExtension || applicableExtensions.Contains(extension));
}

#if !NETSTANDARD2_0 && !NET462
static async Task EmbedAssetIfApplicableAsync(PerformableAsset asset)
{
var bytes = await File.ReadAllBytesAsync(asset.FilePath).ConfigureAwait(false);
#else
static Task EmbedAssetIfApplicableAsync(PerformableAsset asset)
{
var bytes = File.ReadAllBytes(asset.FilePath);
#endif

asset.FileData = Convert.ToBase64String(bytes);
asset.FilePath = null;
#if NETSTANDARD2_0 || NET462
return Task.CompletedTask;
#endif
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<Import Project="..\Tools\PackageReadmes.props" />

<PropertyGroup>
<TargetFrameworks Condition="'$(TargetFrameworks)' == ''">netcoreapp3.1;net462;netstandard2.0;net6.0;net8.0</TargetFrameworks>
<TargetFrameworks Condition="'$(TargetFrameworks)' == ''">net462;netstandard2.0;net6.0;net8.0</TargetFrameworks>
<OutputType Condition="'$(TargetFramework)' != 'netstandard2.0'">Exe</OutputType>
<OutputType Condition="'$(TargetFramework)' == 'netstandard2.0'">Library</OutputType>
<NoWarn>NU1903,NU1902</NoWarn>
Expand All @@ -18,14 +18,15 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.0" Condition="'$(TargetFramework)' != 'netstandard2.0'" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" Condition="'$(TargetFramework)' == 'net462'" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CSF.Screenplay.JsonToHtmlReport.Template\CSF.Screenplay.JsonToHtmlReport.Template.proj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
<ProjectReference Include="..\CSF.Screenplay\CSF.Screenplay.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
20 changes: 20 additions & 0 deletions CSF.Screenplay.JsonToHtmlReport/IEmbedsReportAssets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Threading.Tasks;
using CSF.Screenplay.ReportModel;

namespace CSF.Screenplay.JsonToHtmlReport
{
/// <summary>
/// A service which reworks a <see cref="ScreenplayReport"/>, embedding assets within the report.
/// </summary>
public interface IEmbedsReportAssets
{
/// <summary>
/// Processes the specified <see cref="ScreenplayReport"/>, converting external file assets to embedded assets,
/// where they meet the criteria specified by the specified <see cref="ReportConverterOptions"/>.
/// </summary>
/// <param name="report">A Screenplay report</param>
/// <param name="options">A set of options for converting a report to HTML</param>
/// <returns>A task which completes when the process is done.</returns>
Task EmbedReportAssetsAsync(ScreenplayReport report, ReportConverterOptions options);
}
}
36 changes: 28 additions & 8 deletions CSF.Screenplay.JsonToHtmlReport/ReportConverter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Threading.Tasks;
using CSF.Screenplay.ReportModel;
using CSF.Screenplay.Reporting;

namespace CSF.Screenplay.JsonToHtmlReport
{
Expand All @@ -10,26 +12,35 @@ namespace CSF.Screenplay.JsonToHtmlReport
public class ReportConverter : IConvertsReportJsonToHtml
{
readonly IGetsHtmlTemplate templateReader;
readonly IEmbedsReportAssets assetsEmbedder;
readonly IDeserializesReport reportReader;
readonly ISerializesReport reportWriter;

/// <inheritdoc/>
public async Task ConvertAsync(ReportConverterOptions options)
{
var report = await ReadReport(options.ReportPath).ConfigureAwait(false);
await assetsEmbedder.EmbedReportAssetsAsync(report, options).ConfigureAwait(false);
var reportJson = await GetModifiedReportJsonAsync(report).ConfigureAwait(false);
var template = await templateReader.ReadTemplate().ConfigureAwait(false);
var assembledTemplate = template.Replace("<!-- REPORT_PLACEHOLDER -->", report);
var assembledTemplate = template.Replace("<!-- REPORT_PLACEHOLDER -->", reportJson);
await WriteReport(options.OutputPath, assembledTemplate).ConfigureAwait(false);
}

static async Task<string> ReadReport(string path)
async Task<ScreenplayReport> ReadReport(string path)
{
using (var stream = File.OpenRead(path))
using (var reader = new StreamReader(stream))
{
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
return await reportReader.DeserializeAsync(stream).ConfigureAwait(false);
}

async Task<string> GetModifiedReportJsonAsync(ScreenplayReport report)
{
var stream = await reportWriter.SerializeAsync(report).ConfigureAwait(false);
using(var textReader = new StreamReader(stream))
return await textReader.ReadToEndAsync().ConfigureAwait(false);
}

static async Task WriteReport(string path, string report)
async Task WriteReport(string path, string report)
{
using (var stream = File.Create(path))
using (var writer = new StreamWriter(stream))
Expand All @@ -42,9 +53,18 @@ static async Task WriteReport(string path, string report)
/// Initializes a new instance of the <see cref="ReportConverter"/> class.
/// </summary>
/// <param name="templateReader">The template reader used to get the HTML template.</param>
public ReportConverter(IGetsHtmlTemplate templateReader)
/// <param name="assetsEmbedder">A service which embeds asset data into the JSON report.</param>
/// <param name="reportReader">A report deserializer</param>
/// <param name="reportWriter">A report serializer</param>
public ReportConverter(IGetsHtmlTemplate templateReader,
IEmbedsReportAssets assetsEmbedder,
IDeserializesReport reportReader,
ISerializesReport reportWriter)
{
this.templateReader = templateReader ?? throw new ArgumentNullException(nameof(templateReader));
this.assetsEmbedder = assetsEmbedder ?? throw new ArgumentNullException(nameof(assetsEmbedder));
this.reportReader = reportReader ?? throw new ArgumentNullException(nameof(reportReader));
this.reportWriter = reportWriter ?? throw new ArgumentNullException(nameof(reportWriter));
}
}
}
72 changes: 72 additions & 0 deletions CSF.Screenplay.JsonToHtmlReport/ReportConverterOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace CSF.Screenplay.JsonToHtmlReport
{
/// <summary>
Expand All @@ -14,5 +18,73 @@ public class ReportConverterOptions
/// Gets or sets the file system path where the HTML report will be saved.
/// </summary>
public string OutputPath { get; set; } = "ScreenplayReport.html";

/// <summary>
/// Gets or sets a threshold (in Kilobytes) for files which should be embedded into the report.
/// </summary>
/// <remarks>
/// <para>
/// By default, the report converter will attempt to embed file data into the HTML report file.
/// This is desirable because it means that the report file is likely to be portable as a single file, even when
/// <xref href="AssetsArticle?text=it+contains+linked+assets"/>. Any asset files of supported file extensions are
/// embedded into the HTML file if their file size (in kilobytes) is less than or equal to this value.
/// </para>
/// <para>
/// The default value for this property is 500 (500 kilobytes, half a megabyte). Setting this value to zero
/// or a negative number will disable embedding of files into the report.
/// Setting this value to an arbitrarily high number (such as 1000000, meaning a gigabyte) will cause all files to be
/// embedded.
/// </para>
/// <para>
/// The supported file extensions are listed in the option property <see cref="EmbeddedFileExtensions"/>.
/// </para>
/// </remarks>
public int EmbeddedFileSizeThresholdKb { get; set; } = 500;

/// <summary>
/// Gets or sets a comma-separated list of file extensions which are supported for embedding into report.
/// </summary>
/// <remarks>
/// <para>
/// By default, the report converter will attempt to embed file data into the HTML report file.
/// This is desirable because it means that the report file is likely to be portable as a single file, even when
/// <xref href="AssetsArticle?text=it+contains+linked+assets"/>. Any asset files with a size less than or equal to the threshold
/// are embedded into the HTML file if their file extension is amongst those listed in this property.
/// </para>
/// <para>
/// The default value for this property is <c>jpg,jpeg,png,gif,webp,svg,mp4,mov,avi,wmv,mkv,webm</c>.
/// These are common image and video file types, seen on the web.
/// Note that the wildcard <c>*</c>, if included anywhere this property value, denotes that files of all (any) extension
/// should be embedded into the report.
/// Setting this value to an empty string will disable embedding of files into the report.
/// </para>
/// <para>
/// The file-size threshold for files which may be embedded into the report is controlled by the option property
/// <see cref="EmbeddedFileSizeThresholdKb"/>.
/// </para>
/// </remarks>
public string EmbeddedFileExtensions { get; set; } = "jpg,jpeg,png,gif,webp,svg,mp4,mov,avi,wmv,mkv,webm";

/// <summary>
/// Gets a collection of file extensions (including the leading period) which should be embedded into HTML reports.
/// </summary>
/// <returns>A collection of the extensions to embed</returns>
public IReadOnlyCollection<string> GetEmbeddedFileExtensions()
{
if(string.IsNullOrWhiteSpace(EmbeddedFileExtensions)) return Array.Empty<string>();
return EmbeddedFileExtensions.Split(',').Select(x => string.Concat(".", x.Trim())).ToArray();
}

/// <summary>
/// Gets a value indicating whether all file types (regardless of extension) should be embedded.
/// </summary>
/// <remarks>
/// <para>
/// This method returns <see langword="true"/> if <see cref="EmbeddedFileExtensions"/> contains the character <c>*</c>.
/// Note that a file must still have a size equal to or less than <see cref="EmbeddedFileSizeThresholdKb"/> to be embedded.
/// </para>
/// </remarks>
/// <returns><see langword="true"/> if all file types are to be embedded; <see langword="false"/> if not.</returns>
public bool ShouldEmbedAllFileTypes() => EmbeddedFileExtensions.Contains('*');
}
}
Loading
Loading