Skip to content

[Java, .Net] Support customizations #409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Aug 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
20afb81
Support customizations
mpkorstanje Aug 10, 2025
a91a764
Harmonize with https://github.com/cucumber/html-formatter/pull/406
mpkorstanje Aug 10, 2025
fa4aca2
Fix platform dependent eols
mpkorstanje Aug 10, 2025
18f5140
Run lint fix
mpkorstanje Aug 10, 2025
337eabe
Fix more
mpkorstanje Aug 10, 2025
cc345c1
Fix more
mpkorstanje Aug 10, 2025
7bb0e6a
Fix whitespace issues
mpkorstanje Aug 10, 2025
945bb3a
Fix paths for js
mpkorstanje Aug 10, 2025
87e6565
Implement customizations and builder, converted to idiomatic .NET "se…
gasparnagy Aug 11, 2025
86f8596
Fix acceptance tests for js
mpkorstanje Aug 11, 2025
1419b29
Fix lint for js
mpkorstanje Aug 11, 2025
d291f91
Customize script and default script in java
mpkorstanje Aug 11, 2025
94fa3c8
Update documentation
mpkorstanje Aug 11, 2025
c2f347a
Merge remote-tracking branch 'origin/main' into optional-customizations
mpkorstanje Aug 11, 2025
5749fc5
Fix merge
mpkorstanje Aug 11, 2025
5648355
Fix weird doc
mpkorstanje Aug 11, 2025
8d5ddf4
Allow customizing Javascript and CSS resource loader
gasparnagy Aug 11, 2025
d8071d4
Update more docs
mpkorstanje Aug 11, 2025
3e35e66
Simplify make file
mpkorstanje Aug 11, 2025
0253d9b
Reduce diff on MessagesToHtmlWriter
mpkorstanje Aug 11, 2025
890f024
Reduce diff on MessagesToHtmlWriter
mpkorstanje Aug 11, 2025
88a665b
Reduce diff on MessagesToHtmlWriter
mpkorstanje Aug 11, 2025
3c6d7a0
Reduce diff on MessagesToHtmlWriter
mpkorstanje Aug 11, 2025
aee2b0a
Fix make in CI?
mpkorstanje Aug 11, 2025
e5c2312
Simplify make, differently
mpkorstanje Aug 11, 2025
5b31bb6
Debug make
mpkorstanje Aug 11, 2025
95215ef
Debug make
mpkorstanje Aug 11, 2025
ed6ae72
Simplify make, differently
mpkorstanje Aug 11, 2025
ea3bda4
Simplify make, differently
mpkorstanje Aug 11, 2025
9b95864
Simplify make, differently
mpkorstanje Aug 11, 2025
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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [.Net] Support customizations ([#409](https://github.com/cucumber/html-formatter/pull/409))
- [Java] Support customizations ([#409](https://github.com/cucumber/html-formatter/pull/409))

### Changed
- Upgrade `react-components` to [23.2.0](https://github.com/cucumber/react-components/releases/tag/v23.2.0)

Expand Down
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
javascript_source = $(wildcard javascript/src/*)
assets = main.css main.js main.js.LICENSE.txt index.mustache.html
assets = main.css main.js main.js.LICENSE.txt index.mustache.html icon.url
ruby_assets = $(addprefix ruby/assets/,$(assets))
java_assets = $(addprefix java/src/main/resources/io/cucumber/htmlformatter/,$(assets))
dotnet_assets = $(addprefix dotnet/Cucumber.HtmlFormatter/Resources/,$(assets))
Expand All @@ -15,24 +15,28 @@ clean: ## Remove javascript built module and related artifacts from java and rub
rm -rf $(ruby_assets) $(java_assets) $(dotnet_assets) javascript/dist
.PHONY: .clean

ruby/assets/index.mustache.html: javascript/src/index.mustache.html
ruby/assets/%: javascript/dist/src/%
cp $< $@

ruby/assets/%: javascript/dist/%
cp $< $@

java/src/main/resources/io/cucumber/htmlformatter/index.mustache.html: javascript/src/index.mustache.html
java/src/main/resources/io/cucumber/htmlformatter/%: javascript/dist/src/%
cp $< $@

java/src/main/resources/io/cucumber/htmlformatter/%: javascript/dist/%
cp $< $@

dotnet/Cucumber.HtmlFormatter/Resources/index.mustache.html: javascript/src/index.mustache.html
dotnet/Cucumber.HtmlFormatter/Resources/%: javascript/dist/src/%
cp $< $@

dotnet/Cucumber.HtmlFormatter/Resources/%: javascript/dist/%
cp $< $@

javascript/dist/src/index.mustache.html: javascript/dist/main.js

javascript/dist/src/icon.url: javascript/dist/main.js

javascript/dist/main.js.LICENSE.txt: javascript/dist/main.js

javascript/dist/main.css: javascript/dist/main.js
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ This formatter is built into the following Cucumber implementations:
* [cucumber-ruby](https://github.com/cucumber/cucumber-ruby/blob/main/lib/cucumber/formatter/html.rb)
* [cucumber-jvm](https://github.com/cucumber/cucumber-jvm/blob/main/core/src/main/java/io/cucumber/core/plugin/HtmlFormatter.java)
* [cucumber-js](https://github.com/cucumber/cucumber-js/blob/main/src/formatter/html_formatter.ts)
* [Reqnroll](https://github.com/reqnroll/Reqnroll/blob/main/Reqnroll/Formatters/Html/HtmlFormatter.cs)

## Customizations

_Supported by: Java and .Net_

The formatter can be configured with:
* A custom page title and icon
* Additional CSS to support [styling react components](https://github.com/cucumber/react-components?tab=readme-ov-file#styling).
* Additional Javascript for other customisations.
* The default Javascript and CSS can be replaced to support building custom react components.

## Contributing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<NoWarn>1591</NoWarn>
<Deterministic Condition="'$(Configuration)' == 'Release'">false</Deterministic>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Cucumber.HtmlFormatter.snk</AssemblyOriginatorKeyFile>
<AssemblyOriginatorKeyFile>..\Cucumber.HtmlFormatter.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<PropertyGroup Label="Version">
Expand Down
70 changes: 70 additions & 0 deletions dotnet/Cucumber.HtmlFormatter/HtmlReportSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
namespace Cucumber.HtmlFormatter;

/// <summary>
/// Settings for HTML report generation
/// </summary>
public class HtmlReportSettings
{
private const string DEFAULT_TITLE = "Cucumber";
private string? _icon = null;

/// <summary>
/// Gets or sets the title of the HTML report.
/// Default is "Cucumber".
/// </summary>
public string Title { get; set; } = DEFAULT_TITLE;

/// <summary>
/// Gets or sets the icon for the HTML report.
/// Default is the Cucumber icon as a base64-encoded SVG.
/// </summary>
public string Icon
{
get => _icon ?? LoadDefaultIcon();
set => _icon = value;
}

/// <summary>
/// Gets or sets custom CSS to include in the HTML report.
/// Default is empty.
/// </summary>
public string CustomCss { get; set; } = string.Empty;

/// <summary>
/// Gets or sets custom Javascript to include after the main Javascript section of the HTML report.
/// Default is empty.
/// </summary>
public string CustomScript { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the function that loads the main Javascript resource for the HTML report.
/// This should only be customized if you use a custom node.js project to serve the HTML report,
/// for smaller customizations you can use the <see cref="CustomScript"/> and <see cref="CustomCss"/> property.
/// </summary>
public Func<string> JavascriptResourceLoader { get; set; }

/// <summary>
/// Gets or sets the function that loads the main CSS resource for the HTML report.
/// This should only be customized if you use a custom node.js project to serve the HTML report,
/// for smaller customizations you can use the <see cref="CustomScript"/> and <see cref="CustomCss"/> property.
/// </summary>
public Func<string> CssResourceLoader { get; set; }

/// <summary>
/// Creates a new instance of HtmlReportSettings with default values.
/// </summary>
public HtmlReportSettings()
{
JavascriptResourceLoader = LoadJavascriptResource;
CssResourceLoader = LoadCssResource;
}

private static string LoadDefaultIcon()
=> MessagesToHtmlWriter.GetResource("icon.url");

private static string LoadJavascriptResource()
=> MessagesToHtmlWriter.GetResource("main.js");

private static string LoadCssResource()
=> MessagesToHtmlWriter.GetResource("main.css");
}
2 changes: 2 additions & 0 deletions dotnet/Cucumber.HtmlFormatter/InternalsVisibleTo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Cucumber.HtmlFormatterTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001003d3d7a4a4bb40fd4ad4b18dea92bd57f04946bbce990a7e72a406f026c0af1544510e1069718f7bdc8134fca21b4fb61d8ff139af7c19f2d855a0bf7539667334371478c323ff84e91ccb6a5bc3027fea39ca84658087b7f7f76c30af7adacb315442a0cbec817b71017f363fc4d8a751b98b6e60b00149b08c84ca984ab5ddd")]
86 changes: 50 additions & 36 deletions dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,70 +9,91 @@ public class MessagesToHtmlWriter : IDisposable
private readonly Action<StreamWriter, Envelope> _streamSerializer;
private readonly string _template;
private readonly JsonInHtmlWriter _jsonInHtmlWriter;
private readonly HtmlReportSettings _settings;
private bool _streamClosed = false;
private bool _preMessageWritten = false;
private bool _firstMessageWritten = false;
private bool _postMessageWritten = false;
private readonly bool _isAsyncInitialized = false;

[Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(Stream, Func<StreamWriter, Envelope, Task>) constructor", false)]
public MessagesToHtmlWriter(Stream stream, Action<StreamWriter, Envelope> streamSerializer) : this(new StreamWriter(stream), streamSerializer)
{
}
public MessagesToHtmlWriter(Stream stream, Func<StreamWriter, Envelope, Task> asyncStreamSerializer) : this(new StreamWriter(stream), asyncStreamSerializer) { }
public MessagesToHtmlWriter(Stream stream, Action<StreamWriter, Envelope> streamSerializer)
: this(new StreamWriter(stream), streamSerializer) { }

public MessagesToHtmlWriter(Stream stream, Func<StreamWriter, Envelope, Task> asyncStreamSerializer, HtmlReportSettings? settings = null)
: this(new StreamWriter(stream), asyncStreamSerializer, settings) { }

[Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(StreamWriter, Func<StreamWriter, Envelope, Task>) constructor", false)]
public MessagesToHtmlWriter(StreamWriter writer, Action<StreamWriter, Envelope> streamSerializer)
{
this._writer = writer;
this._streamSerializer = streamSerializer;
_writer = writer;
_streamSerializer = streamSerializer;
_settings = new HtmlReportSettings();
// 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 = LoadTemplateResource();
_jsonInHtmlWriter = new JsonInHtmlWriter(writer);
_isAsyncInitialized = false;
}
public MessagesToHtmlWriter(StreamWriter writer, Func<StreamWriter, Envelope, Task> asyncStreamSerializer)

public MessagesToHtmlWriter(StreamWriter writer, Func<StreamWriter, Envelope, Task> asyncStreamSerializer, HtmlReportSettings? settings = null)
{
this._writer = writer;
this._asyncStreamSerializer = asyncStreamSerializer;
_writer = writer;
_asyncStreamSerializer = asyncStreamSerializer;
_settings = settings ?? new();
// 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();
_template = LoadTemplateResource();
_jsonInHtmlWriter = new JsonInHtmlWriter(writer);
_isAsyncInitialized = true;
}

private void WritePreMessage()
{
WriteTemplateBetween(_writer, _template, null, "{{css}}");
WriteResource(_writer, "main.css");
WriteTemplateBetween(_writer, _template, "{{css}}", "{{messages}}");
WriteTemplateBetween(_writer, _template, null, "{{title}}");
_writer.Write(_settings.Title);
WriteTemplateBetween(_writer, _template, "{{title}}", "{{icon}}");
_writer.Write(_settings.Icon);
WriteTemplateBetween(_writer, _template, "{{icon}}", "{{css}}");
_writer.Write(_settings.CssResourceLoader());
WriteTemplateBetween(_writer, _template, "{{css}}", "{{custom_css}}");
_writer.Write(_settings.CustomCss);
WriteTemplateBetween(_writer, _template, "{{custom_css}}", "{{messages}}");
}

private async Task WritePreMessageAsync()
{
await WriteTemplateBetweenAsync(_writer, _template, null, "{{css}}");
await WriteResourceAsync(_writer, "main.css");
await WriteTemplateBetweenAsync(_writer, _template, "{{css}}", "{{messages}}");
await WriteTemplateBetweenAsync(_writer, _template, null, "{{title}}");
await _writer.WriteAsync(_settings.Title);
await WriteTemplateBetweenAsync(_writer, _template, "{{title}}", "{{icon}}");
await _writer.WriteAsync(_settings.Icon);
await WriteTemplateBetweenAsync(_writer, _template, "{{icon}}", "{{css}}");
await _writer.WriteAsync(_settings.CssResourceLoader());
await WriteTemplateBetweenAsync(_writer, _template, "{{css}}", "{{custom_css}}");
await _writer.WriteAsync(_settings.CustomCss);
await WriteTemplateBetweenAsync(_writer, _template, "{{custom_css}}", "{{messages}}");
}

private void WritePostMessage()
{
WriteTemplateBetween(_writer, _template, "{{messages}}", "{{script}}");
WriteResource(_writer, "main.js");
WriteTemplateBetween(_writer, _template, "{{script}}", null);
_writer.Write(_settings.JavascriptResourceLoader());
WriteTemplateBetween(_writer, _template, "{{script}}", "{{custom_script}}");
_writer.Write(_settings.CustomScript);
WriteTemplateBetween(_writer, _template, "{{custom_script}}", null);
}

private async Task WritePostMessageAsync()
{
await WriteTemplateBetweenAsync(_writer, _template, "{{messages}}", "{{script}}");
await WriteResourceAsync(_writer, "main.js");
await WriteTemplateBetweenAsync(_writer, _template, "{{script}}", null);
await _writer.WriteAsync(_settings.JavascriptResourceLoader());
await WriteTemplateBetweenAsync(_writer, _template, "{{script}}", "{{custom_script}}");
await _writer.WriteAsync(_settings.CustomScript);
await WriteTemplateBetweenAsync(_writer, _template, "{{custom_script}}", null);
}

public void Write(Envelope envelope)
Expand Down Expand Up @@ -186,18 +207,6 @@ public async Task DisposeAsync()
}
}

private void WriteResource(StreamWriter writer, string v)
{
var resource = GetResource(v);
writer.Write(resource);
}

private async Task WriteResourceAsync(StreamWriter writer, string v)
{
var resource = GetResource(v);
await writer.WriteAsync(resource);
}

private void WriteTemplateBetween(StreamWriter writer, string template, string? begin, string? end)
{
CalculateBeginAndLength(template, begin, end, out var beginIndex, out var lengthToWrite);
Expand All @@ -217,7 +226,7 @@ private async Task WriteTemplateBetweenAsync(StreamWriter writer, string templat
await writer.WriteAsync(template.Substring(beginIndex, lengthToWrite));
}

private string GetResource(string name)
internal static string GetResource(string name)
{
var assembly = typeof(MessagesToHtmlWriter).Assembly;
var resourceStream = assembly.GetManifestResourceStream("Cucumber.HtmlFormatter.Resources." + name);
Expand All @@ -226,4 +235,9 @@ private string GetResource(string name)
var resource = new StreamReader(resourceStream).ReadToEnd();
return resource;
}

private static string LoadTemplateResource()
{
return GetResource("index.mustache.html");
}
}
3 changes: 2 additions & 1 deletion dotnet/Cucumber.HtmlFormatter/Resources/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
*.html
*.js
*.css
*.txt
*.txt
*.url
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\Cucumber.HtmlFormatter.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading
Loading