From 2c93d93dc769bad6f7a081d6a3db42f1e923d964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Mon, 4 Aug 2025 17:59:32 +0200 Subject: [PATCH 1/5] dotnet: allow customizing resources in subclasses --- .../MessagesToHtmlWriter.cs | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs b/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs index 7c460b22..c5e24195 100644 --- a/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs +++ b/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs @@ -1,4 +1,5 @@ -using Io.Cucumber.Messages.Types; +using System.Reflection; +using Io.Cucumber.Messages.Types; namespace Cucumber.HtmlFormatter; @@ -7,7 +8,7 @@ public class MessagesToHtmlWriter : IDisposable private readonly StreamWriter _writer; private readonly Func _asyncStreamSerializer; private readonly Action _streamSerializer; - private readonly string _template; + private readonly Lazy _template; private readonly JsonInHtmlWriter _jsonInHtmlWriter; private bool _streamClosed = false; private bool _preMessageWritten = false; @@ -15,6 +16,8 @@ public class MessagesToHtmlWriter : IDisposable private bool _postMessageWritten = false; private readonly bool _isAsyncInitialized = false; + private string Template => _template.Value; + [Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(Stream, Func) constructor", false)] public MessagesToHtmlWriter(Stream stream, Action streamSerializer) : this(new StreamWriter(stream), streamSerializer) { @@ -24,15 +27,15 @@ public MessagesToHtmlWriter(Stream stream, Func as [Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(StreamWriter, Func) constructor", false)] public MessagesToHtmlWriter(StreamWriter writer, Action streamSerializer) { - this._writer = writer; - this._streamSerializer = streamSerializer; + _writer = writer; + _streamSerializer = streamSerializer; // Create async wrapper for sync serializer - this._asyncStreamSerializer = (w, e) => + _asyncStreamSerializer = (w, e) => { streamSerializer(w, e); return Task.CompletedTask; }; - _template = GetResource("index.mustache.html"); + _template = new Lazy(() => GetResource("index.mustache.html")); _jsonInHtmlWriter = new JsonInHtmlWriter(writer); _isAsyncInitialized = false; } @@ -42,37 +45,37 @@ public MessagesToHtmlWriter(StreamWriter writer, Func asyncStreamSerializer(w, e).GetAwaiter().GetResult(); - _template = GetResource("index.mustache.html"); + _template = new Lazy(() => GetResource("index.mustache.html")); _jsonInHtmlWriter = new JsonInHtmlWriter(writer); _isAsyncInitialized = true; } private void WritePreMessage() { - WriteTemplateBetween(_writer, _template, null, "{{css}}"); + WriteTemplateBetween(_writer, Template, null, "{{css}}"); WriteResource(_writer, "main.css"); - WriteTemplateBetween(_writer, _template, "{{css}}", "{{messages}}"); + WriteTemplateBetween(_writer, Template, "{{css}}", "{{messages}}"); } private async Task WritePreMessageAsync() { - await WriteTemplateBetweenAsync(_writer, _template, null, "{{css}}"); + await WriteTemplateBetweenAsync(_writer, Template, null, "{{css}}"); await WriteResourceAsync(_writer, "main.css"); - await WriteTemplateBetweenAsync(_writer, _template, "{{css}}", "{{messages}}"); + await WriteTemplateBetweenAsync(_writer, Template, "{{css}}", "{{messages}}"); } private void WritePostMessage() { - WriteTemplateBetween(_writer, _template, "{{messages}}", "{{script}}"); + WriteTemplateBetween(_writer, Template, "{{messages}}", "{{script}}"); WriteResource(_writer, "main.js"); - WriteTemplateBetween(_writer, _template, "{{script}}", null); + WriteTemplateBetween(_writer, Template, "{{script}}", null); } private async Task WritePostMessageAsync() { - await WriteTemplateBetweenAsync(_writer, _template, "{{messages}}", "{{script}}"); + await WriteTemplateBetweenAsync(_writer, Template, "{{messages}}", "{{script}}"); await WriteResourceAsync(_writer, "main.js"); - await WriteTemplateBetweenAsync(_writer, _template, "{{script}}", null); + await WriteTemplateBetweenAsync(_writer, Template, "{{script}}", null); } public void Write(Envelope envelope) @@ -217,9 +220,21 @@ private async Task WriteTemplateBetweenAsync(StreamWriter writer, string templat await writer.WriteAsync(template.Substring(beginIndex, lengthToWrite)); } - private string GetResource(string name) + /// + /// Returns the assembly where the resources are located. The default implementation returns the package assembly ('Cucumber.HtmlFormatter'). + /// + protected virtual Assembly GetResourceAssembly() + { + return typeof(MessagesToHtmlWriter).Assembly; + } + + /// + /// Returns a resource for the generated HTML page. + /// + /// The resource file name. Either 'index.mustache.html', 'main.css' or 'main.js'. + protected virtual string GetResource(string name) { - var assembly = typeof(MessagesToHtmlWriter).Assembly; + var assembly = GetResourceAssembly(); var resourceStream = assembly.GetManifestResourceStream("Cucumber.HtmlFormatter.Resources." + name); if (resourceStream == null) throw new InvalidOperationException($"Resource '{name}' not found in assembly '{assembly.FullName}'"); From f8bd7e91b0b85dd5b00b17897a8b85ada691b2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Mon, 4 Aug 2025 18:03:17 +0200 Subject: [PATCH 2/5] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e03dbd..767e385a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- [.Net] Added possiblity to customize resources in subclasses of MessagesToHtmlWriter ([#400](https://github.com/cucumber/html-formatter/pull/400)) ## [21.13.0] - 2025-07-07 ### Changed From f1f7169c82db9d304d9883e5fb7d37378de8a7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Tue, 5 Aug 2025 11:06:15 +0200 Subject: [PATCH 3/5] Revert "dotnet: allow customizing resources in subclasses" This reverts commit 2c93d93dc769bad6f7a081d6a3db42f1e923d964. --- .../MessagesToHtmlWriter.cs | 49 +++++++------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs b/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs index c5e24195..7c460b22 100644 --- a/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs +++ b/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs @@ -1,5 +1,4 @@ -using System.Reflection; -using Io.Cucumber.Messages.Types; +using Io.Cucumber.Messages.Types; namespace Cucumber.HtmlFormatter; @@ -8,7 +7,7 @@ public class MessagesToHtmlWriter : IDisposable private readonly StreamWriter _writer; private readonly Func _asyncStreamSerializer; private readonly Action _streamSerializer; - private readonly Lazy _template; + private readonly string _template; private readonly JsonInHtmlWriter _jsonInHtmlWriter; private bool _streamClosed = false; private bool _preMessageWritten = false; @@ -16,8 +15,6 @@ public class MessagesToHtmlWriter : IDisposable private bool _postMessageWritten = false; private readonly bool _isAsyncInitialized = false; - private string Template => _template.Value; - [Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(Stream, Func) constructor", false)] public MessagesToHtmlWriter(Stream stream, Action streamSerializer) : this(new StreamWriter(stream), streamSerializer) { @@ -27,15 +24,15 @@ public MessagesToHtmlWriter(Stream stream, Func as [Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(StreamWriter, Func) constructor", false)] public MessagesToHtmlWriter(StreamWriter writer, Action streamSerializer) { - _writer = writer; - _streamSerializer = streamSerializer; + this._writer = writer; + this._streamSerializer = streamSerializer; // Create async wrapper for sync serializer - _asyncStreamSerializer = (w, e) => + this._asyncStreamSerializer = (w, e) => { streamSerializer(w, e); return Task.CompletedTask; }; - _template = new Lazy(() => GetResource("index.mustache.html")); + _template = GetResource("index.mustache.html"); _jsonInHtmlWriter = new JsonInHtmlWriter(writer); _isAsyncInitialized = false; } @@ -45,37 +42,37 @@ public MessagesToHtmlWriter(StreamWriter writer, Func asyncStreamSerializer(w, e).GetAwaiter().GetResult(); - _template = new Lazy(() => GetResource("index.mustache.html")); + _template = GetResource("index.mustache.html"); _jsonInHtmlWriter = new JsonInHtmlWriter(writer); _isAsyncInitialized = true; } private void WritePreMessage() { - WriteTemplateBetween(_writer, Template, null, "{{css}}"); + WriteTemplateBetween(_writer, _template, null, "{{css}}"); WriteResource(_writer, "main.css"); - WriteTemplateBetween(_writer, Template, "{{css}}", "{{messages}}"); + WriteTemplateBetween(_writer, _template, "{{css}}", "{{messages}}"); } private async Task WritePreMessageAsync() { - await WriteTemplateBetweenAsync(_writer, Template, null, "{{css}}"); + await WriteTemplateBetweenAsync(_writer, _template, null, "{{css}}"); await WriteResourceAsync(_writer, "main.css"); - await WriteTemplateBetweenAsync(_writer, Template, "{{css}}", "{{messages}}"); + await WriteTemplateBetweenAsync(_writer, _template, "{{css}}", "{{messages}}"); } private void WritePostMessage() { - WriteTemplateBetween(_writer, Template, "{{messages}}", "{{script}}"); + WriteTemplateBetween(_writer, _template, "{{messages}}", "{{script}}"); WriteResource(_writer, "main.js"); - WriteTemplateBetween(_writer, Template, "{{script}}", null); + WriteTemplateBetween(_writer, _template, "{{script}}", null); } private async Task WritePostMessageAsync() { - await WriteTemplateBetweenAsync(_writer, Template, "{{messages}}", "{{script}}"); + await WriteTemplateBetweenAsync(_writer, _template, "{{messages}}", "{{script}}"); await WriteResourceAsync(_writer, "main.js"); - await WriteTemplateBetweenAsync(_writer, Template, "{{script}}", null); + await WriteTemplateBetweenAsync(_writer, _template, "{{script}}", null); } public void Write(Envelope envelope) @@ -220,21 +217,9 @@ private async Task WriteTemplateBetweenAsync(StreamWriter writer, string templat await writer.WriteAsync(template.Substring(beginIndex, lengthToWrite)); } - /// - /// Returns the assembly where the resources are located. The default implementation returns the package assembly ('Cucumber.HtmlFormatter'). - /// - protected virtual Assembly GetResourceAssembly() - { - return typeof(MessagesToHtmlWriter).Assembly; - } - - /// - /// Returns a resource for the generated HTML page. - /// - /// The resource file name. Either 'index.mustache.html', 'main.css' or 'main.js'. - protected virtual string GetResource(string name) + private string GetResource(string name) { - var assembly = GetResourceAssembly(); + var assembly = typeof(MessagesToHtmlWriter).Assembly; var resourceStream = assembly.GetManifestResourceStream("Cucumber.HtmlFormatter.Resources." + name); if (resourceStream == null) throw new InvalidOperationException($"Resource '{name}' not found in assembly '{assembly.FullName}'"); From e8d88240c95fca89e3f10d4d44be5f41e36704f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Tue, 5 Aug 2025 11:55:58 +0200 Subject: [PATCH 4/5] Make resource provider customizable without sublclassing MessagesToHtmlWriter --- CHANGELOG.md | 2 +- .../DefaultResourceProvider.cs | 65 ++++++++++++++++++ .../IResourceProvider.cs | 29 ++++++++ .../MessagesToHtmlWriter.cs | 55 +++++++-------- .../DefaultResourceProviderTest.cs | 67 +++++++++++++++++++ 5 files changed, 187 insertions(+), 31 deletions(-) create mode 100644 dotnet/Cucumber.HtmlFormatter/DefaultResourceProvider.cs create mode 100644 dotnet/Cucumber.HtmlFormatter/IResourceProvider.cs create mode 100644 dotnet/Cucumber.HtmlFormatterTest/DefaultResourceProviderTest.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 767e385a..65f74373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added -- [.Net] Added possiblity to customize resources in subclasses of MessagesToHtmlWriter ([#400](https://github.com/cucumber/html-formatter/pull/400)) +- [.Net] Added possibility to customize resources for MessagesToHtmlWriter ([#400](https://github.com/cucumber/html-formatter/pull/400)) ## [21.13.0] - 2025-07-07 ### Changed diff --git a/dotnet/Cucumber.HtmlFormatter/DefaultResourceProvider.cs b/dotnet/Cucumber.HtmlFormatter/DefaultResourceProvider.cs new file mode 100644 index 00000000..a1c64de1 --- /dev/null +++ b/dotnet/Cucumber.HtmlFormatter/DefaultResourceProvider.cs @@ -0,0 +1,65 @@ +using System.Reflection; + +namespace Cucumber.HtmlFormatter; + +/// +/// Default implementation of IResourceProvider +/// +public class DefaultResourceProvider : IResourceProvider +{ + private const string TEMPLATE_RESOURCE_NAME = "index.mustache.html"; + private const string CSS_RESOURCE_NAME = "main.css"; + private const string JAVASCRIPT_RESOURCE_NAME = "main.js"; + private readonly Assembly _assembly; + private readonly string _resourceNamespace; + + public DefaultResourceProvider() + { + _assembly = typeof(MessagesToHtmlWriter).Assembly; + _resourceNamespace = "Cucumber.HtmlFormatter.Resources."; + } + + /// + /// Constructor with custom assembly and namespace + /// + /// The assembly to load resources from + /// The namespace prefix for resources + public DefaultResourceProvider(Assembly assembly, string resourceNamespace) + { + _assembly = assembly; + _resourceNamespace = resourceNamespace; + } + + /// + public string GetTemplateResource() + { + return GetResource(TEMPLATE_RESOURCE_NAME); + } + + /// + public string GetCssResource() + { + return GetResource(CSS_RESOURCE_NAME); + } + + /// + public string GetJavaScriptResource() + { + return GetResource(JAVASCRIPT_RESOURCE_NAME); + } + + /// + /// Gets a resource from the assembly + /// + /// The resource name + /// The resource content + protected string GetResource(string name) + { + var resourceStream = _assembly.GetManifestResourceStream(_resourceNamespace + name); + if (resourceStream == null) + throw new InvalidOperationException($"Resource '{name}' not found in assembly '{_assembly.FullName}'"); + + using var reader = new StreamReader(resourceStream); + return reader.ReadToEnd(); + } +} \ No newline at end of file diff --git a/dotnet/Cucumber.HtmlFormatter/IResourceProvider.cs b/dotnet/Cucumber.HtmlFormatter/IResourceProvider.cs new file mode 100644 index 00000000..275a0be4 --- /dev/null +++ b/dotnet/Cucumber.HtmlFormatter/IResourceProvider.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using System.IO; + +namespace Cucumber.HtmlFormatter; + +/// +/// Interface for providing resources to the HTML formatter +/// +public interface IResourceProvider +{ + /// + /// Gets the HTML template + /// + /// The HTML template + string GetTemplateResource(); + + /// + /// Gets the CSS resource + /// + /// The CSS resource + string GetCssResource(); + + /// + /// Gets the JavaScript resource + /// + /// The JavaScript resource + string GetJavaScriptResource(); +} \ No newline at end of file diff --git a/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs b/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs index 7c460b22..d2629d2d 100644 --- a/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs +++ b/dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs @@ -9,6 +9,7 @@ public class MessagesToHtmlWriter : IDisposable private readonly Action _streamSerializer; private readonly string _template; private readonly JsonInHtmlWriter _jsonInHtmlWriter; + private readonly IResourceProvider _resourceProvider; private bool _streamClosed = false; private bool _preMessageWritten = false; private bool _firstMessageWritten = false; @@ -19,30 +20,36 @@ public class MessagesToHtmlWriter : IDisposable public MessagesToHtmlWriter(Stream stream, Action streamSerializer) : this(new StreamWriter(stream), streamSerializer) { } - public MessagesToHtmlWriter(Stream stream, Func asyncStreamSerializer) : this(new StreamWriter(stream), asyncStreamSerializer) { } + + public MessagesToHtmlWriter(Stream stream, Func asyncStreamSerializer, IResourceProvider? resourceProvider = null) + : this(new StreamWriter(stream), asyncStreamSerializer, resourceProvider) + { } [Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(StreamWriter, Func) constructor", false)] public MessagesToHtmlWriter(StreamWriter writer, Action streamSerializer) { - this._writer = writer; - this._streamSerializer = streamSerializer; + _writer = writer; + _streamSerializer = streamSerializer; // Create async wrapper for sync serializer - this._asyncStreamSerializer = (w, e) => + _asyncStreamSerializer = (w, e) => { streamSerializer(w, e); return Task.CompletedTask; }; - _template = GetResource("index.mustache.html"); + _resourceProvider = new DefaultResourceProvider(); + _template = _resourceProvider.GetTemplateResource(); _jsonInHtmlWriter = new JsonInHtmlWriter(writer); _isAsyncInitialized = false; } - public MessagesToHtmlWriter(StreamWriter writer, Func asyncStreamSerializer) + + public MessagesToHtmlWriter(StreamWriter writer, Func asyncStreamSerializer, IResourceProvider? resourceProvider = null) { - this._writer = writer; - this._asyncStreamSerializer = asyncStreamSerializer; + _writer = writer; + _asyncStreamSerializer = asyncStreamSerializer; // Create sync wrapper for async serializer (will block) - this._streamSerializer = (w, e) => asyncStreamSerializer(w, e).GetAwaiter().GetResult(); - _template = GetResource("index.mustache.html"); + _streamSerializer = (w, e) => asyncStreamSerializer(w, e).GetAwaiter().GetResult(); + _resourceProvider = resourceProvider ?? new DefaultResourceProvider(); + _template = _resourceProvider.GetTemplateResource(); _jsonInHtmlWriter = new JsonInHtmlWriter(writer); _isAsyncInitialized = true; } @@ -50,28 +57,28 @@ public MessagesToHtmlWriter(StreamWriter writer, Func Date: Mon, 11 Aug 2025 10:09:41 +0200 Subject: [PATCH 5/5] Applied review comments --- dotnet/Cucumber.HtmlFormatter/DefaultResourceProvider.cs | 2 +- dotnet/Cucumber.HtmlFormatter/IResourceProvider.cs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/dotnet/Cucumber.HtmlFormatter/DefaultResourceProvider.cs b/dotnet/Cucumber.HtmlFormatter/DefaultResourceProvider.cs index a1c64de1..4fe90e87 100644 --- a/dotnet/Cucumber.HtmlFormatter/DefaultResourceProvider.cs +++ b/dotnet/Cucumber.HtmlFormatter/DefaultResourceProvider.cs @@ -53,7 +53,7 @@ public string GetJavaScriptResource() /// /// The resource name /// The resource content - protected string GetResource(string name) + private string GetResource(string name) { var resourceStream = _assembly.GetManifestResourceStream(_resourceNamespace + name); if (resourceStream == null) diff --git a/dotnet/Cucumber.HtmlFormatter/IResourceProvider.cs b/dotnet/Cucumber.HtmlFormatter/IResourceProvider.cs index 275a0be4..ce9f24ad 100644 --- a/dotnet/Cucumber.HtmlFormatter/IResourceProvider.cs +++ b/dotnet/Cucumber.HtmlFormatter/IResourceProvider.cs @@ -1,11 +1,8 @@ -using System; -using System.Threading.Tasks; -using System.IO; - namespace Cucumber.HtmlFormatter; /// -/// Interface for providing resources to the HTML formatter +/// Interface for providing resources to the HTML formatter. +/// This is an experimental API for allowing customizations, will be replaced by a more flexible solution in an upcoming release. /// public interface IResourceProvider {