Skip to content

Commit 8706dfd

Browse files
[Java, .Net] Support customizations (#409)
* Allows the title and icon in the html report to replaced with a custom title and icon. * Supports replacing and extending the default javascript and css of the report. Closes: #403 Co-authored-by: Gáspár Nagy <[email protected]>
1 parent 1177850 commit 8706dfd

File tree

25 files changed

+627
-162
lines changed

25 files changed

+627
-162
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
9+
### Added
10+
- [.Net] Support customizations ([#409](https://github.com/cucumber/html-formatter/pull/409))
11+
- [Java] Support customizations ([#409](https://github.com/cucumber/html-formatter/pull/409))
12+
913
### Changed
1014
- Upgrade `react-components` to [23.2.0](https://github.com/cucumber/react-components/releases/tag/v23.2.0)
1115

Makefile

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
javascript_source = $(wildcard javascript/src/*)
2-
assets = main.css main.js main.js.LICENSE.txt index.mustache.html
2+
assets = main.css main.js main.js.LICENSE.txt index.mustache.html icon.url
33
ruby_assets = $(addprefix ruby/assets/,$(assets))
44
java_assets = $(addprefix java/src/main/resources/io/cucumber/htmlformatter/,$(assets))
55
dotnet_assets = $(addprefix dotnet/Cucumber.HtmlFormatter/Resources/,$(assets))
@@ -15,24 +15,28 @@ clean: ## Remove javascript built module and related artifacts from java and rub
1515
rm -rf $(ruby_assets) $(java_assets) $(dotnet_assets) javascript/dist
1616
.PHONY: .clean
1717

18-
ruby/assets/index.mustache.html: javascript/src/index.mustache.html
18+
ruby/assets/%: javascript/dist/src/%
1919
cp $< $@
2020

2121
ruby/assets/%: javascript/dist/%
2222
cp $< $@
2323

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

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

30-
dotnet/Cucumber.HtmlFormatter/Resources/index.mustache.html: javascript/src/index.mustache.html
30+
dotnet/Cucumber.HtmlFormatter/Resources/%: javascript/dist/src/%
3131
cp $< $@
3232

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

36+
javascript/dist/src/index.mustache.html: javascript/dist/main.js
37+
38+
javascript/dist/src/icon.url: javascript/dist/main.js
39+
3640
javascript/dist/main.js.LICENSE.txt: javascript/dist/main.js
3741

3842
javascript/dist/main.css: javascript/dist/main.js

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ This formatter is built into the following Cucumber implementations:
1010
* [cucumber-ruby](https://github.com/cucumber/cucumber-ruby/blob/main/lib/cucumber/formatter/html.rb)
1111
* [cucumber-jvm](https://github.com/cucumber/cucumber-jvm/blob/main/core/src/main/java/io/cucumber/core/plugin/HtmlFormatter.java)
1212
* [cucumber-js](https://github.com/cucumber/cucumber-js/blob/main/src/formatter/html_formatter.ts)
13+
* [Reqnroll](https://github.com/reqnroll/Reqnroll/blob/main/Reqnroll/Formatters/Html/HtmlFormatter.cs)
14+
15+
## Customizations
16+
17+
_Supported by: Java and .Net_
18+
19+
The formatter can be configured with:
20+
* A custom page title and icon
21+
* Additional CSS to support [styling react components](https://github.com/cucumber/react-components?tab=readme-ov-file#styling).
22+
* Additional Javascript for other customisations.
23+
* The default Javascript and CSS can be replaced to support building custom react components.
1324

1425
## Contributing
1526

dotnet/Cucumber.HtmlFormatter/Cucumber.HtmlFormatter.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<NoWarn>1591</NoWarn>
1010
<Deterministic Condition="'$(Configuration)' == 'Release'">false</Deterministic>
1111
<SignAssembly>true</SignAssembly>
12-
<AssemblyOriginatorKeyFile>Cucumber.HtmlFormatter.snk</AssemblyOriginatorKeyFile>
12+
<AssemblyOriginatorKeyFile>..\Cucumber.HtmlFormatter.snk</AssemblyOriginatorKeyFile>
1313
</PropertyGroup>
1414

1515
<PropertyGroup Label="Version">
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
namespace Cucumber.HtmlFormatter;
2+
3+
/// <summary>
4+
/// Settings for HTML report generation
5+
/// </summary>
6+
public class HtmlReportSettings
7+
{
8+
private const string DEFAULT_TITLE = "Cucumber";
9+
private string? _icon = null;
10+
11+
/// <summary>
12+
/// Gets or sets the title of the HTML report.
13+
/// Default is "Cucumber".
14+
/// </summary>
15+
public string Title { get; set; } = DEFAULT_TITLE;
16+
17+
/// <summary>
18+
/// Gets or sets the icon for the HTML report.
19+
/// Default is the Cucumber icon as a base64-encoded SVG.
20+
/// </summary>
21+
public string Icon
22+
{
23+
get => _icon ?? LoadDefaultIcon();
24+
set => _icon = value;
25+
}
26+
27+
/// <summary>
28+
/// Gets or sets custom CSS to include in the HTML report.
29+
/// Default is empty.
30+
/// </summary>
31+
public string CustomCss { get; set; } = string.Empty;
32+
33+
/// <summary>
34+
/// Gets or sets custom Javascript to include after the main Javascript section of the HTML report.
35+
/// Default is empty.
36+
/// </summary>
37+
public string CustomScript { get; set; } = string.Empty;
38+
39+
/// <summary>
40+
/// Gets or sets the function that loads the main Javascript resource for the HTML report.
41+
/// This should only be customized if you use a custom node.js project to serve the HTML report,
42+
/// for smaller customizations you can use the <see cref="CustomScript"/> and <see cref="CustomCss"/> property.
43+
/// </summary>
44+
public Func<string> JavascriptResourceLoader { get; set; }
45+
46+
/// <summary>
47+
/// Gets or sets the function that loads the main CSS resource for the HTML report.
48+
/// This should only be customized if you use a custom node.js project to serve the HTML report,
49+
/// for smaller customizations you can use the <see cref="CustomScript"/> and <see cref="CustomCss"/> property.
50+
/// </summary>
51+
public Func<string> CssResourceLoader { get; set; }
52+
53+
/// <summary>
54+
/// Creates a new instance of HtmlReportSettings with default values.
55+
/// </summary>
56+
public HtmlReportSettings()
57+
{
58+
JavascriptResourceLoader = LoadJavascriptResource;
59+
CssResourceLoader = LoadCssResource;
60+
}
61+
62+
private static string LoadDefaultIcon()
63+
=> MessagesToHtmlWriter.GetResource("icon.url");
64+
65+
private static string LoadJavascriptResource()
66+
=> MessagesToHtmlWriter.GetResource("main.js");
67+
68+
private static string LoadCssResource()
69+
=> MessagesToHtmlWriter.GetResource("main.css");
70+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
using System.Runtime.CompilerServices;
2+
[assembly: InternalsVisibleTo("Cucumber.HtmlFormatterTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001003d3d7a4a4bb40fd4ad4b18dea92bd57f04946bbce990a7e72a406f026c0af1544510e1069718f7bdc8134fca21b4fb61d8ff139af7c19f2d855a0bf7539667334371478c323ff84e91ccb6a5bc3027fea39ca84658087b7f7f76c30af7adacb315442a0cbec817b71017f363fc4d8a751b98b6e60b00149b08c84ca984ab5ddd")]

dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,70 +9,91 @@ public class MessagesToHtmlWriter : IDisposable
99
private readonly Action<StreamWriter, Envelope> _streamSerializer;
1010
private readonly string _template;
1111
private readonly JsonInHtmlWriter _jsonInHtmlWriter;
12+
private readonly HtmlReportSettings _settings;
1213
private bool _streamClosed = false;
1314
private bool _preMessageWritten = false;
1415
private bool _firstMessageWritten = false;
1516
private bool _postMessageWritten = false;
1617
private readonly bool _isAsyncInitialized = false;
1718

1819
[Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(Stream, Func<StreamWriter, Envelope, Task>) constructor", false)]
19-
public MessagesToHtmlWriter(Stream stream, Action<StreamWriter, Envelope> streamSerializer) : this(new StreamWriter(stream), streamSerializer)
20-
{
21-
}
22-
public MessagesToHtmlWriter(Stream stream, Func<StreamWriter, Envelope, Task> asyncStreamSerializer) : this(new StreamWriter(stream), asyncStreamSerializer) { }
20+
public MessagesToHtmlWriter(Stream stream, Action<StreamWriter, Envelope> streamSerializer)
21+
: this(new StreamWriter(stream), streamSerializer) { }
22+
23+
public MessagesToHtmlWriter(Stream stream, Func<StreamWriter, Envelope, Task> asyncStreamSerializer, HtmlReportSettings? settings = null)
24+
: this(new StreamWriter(stream), asyncStreamSerializer, settings) { }
2325

2426
[Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(StreamWriter, Func<StreamWriter, Envelope, Task>) constructor", false)]
2527
public MessagesToHtmlWriter(StreamWriter writer, Action<StreamWriter, Envelope> streamSerializer)
2628
{
27-
this._writer = writer;
28-
this._streamSerializer = streamSerializer;
29+
_writer = writer;
30+
_streamSerializer = streamSerializer;
31+
_settings = new HtmlReportSettings();
2932
// Create async wrapper for sync serializer
30-
this._asyncStreamSerializer = (w, e) =>
33+
_asyncStreamSerializer = (w, e) =>
3134
{
3235
streamSerializer(w, e);
3336
return Task.CompletedTask;
3437
};
35-
_template = GetResource("index.mustache.html");
38+
_template = LoadTemplateResource();
3639
_jsonInHtmlWriter = new JsonInHtmlWriter(writer);
3740
_isAsyncInitialized = false;
3841
}
39-
public MessagesToHtmlWriter(StreamWriter writer, Func<StreamWriter, Envelope, Task> asyncStreamSerializer)
42+
43+
public MessagesToHtmlWriter(StreamWriter writer, Func<StreamWriter, Envelope, Task> asyncStreamSerializer, HtmlReportSettings? settings = null)
4044
{
41-
this._writer = writer;
42-
this._asyncStreamSerializer = asyncStreamSerializer;
45+
_writer = writer;
46+
_asyncStreamSerializer = asyncStreamSerializer;
47+
_settings = settings ?? new();
4348
// Create sync wrapper for async serializer (will block)
44-
this._streamSerializer = (w, e) => asyncStreamSerializer(w, e).GetAwaiter().GetResult();
45-
_template = GetResource("index.mustache.html");
49+
_streamSerializer = (w, e) => asyncStreamSerializer(w, e).GetAwaiter().GetResult();
50+
_template = LoadTemplateResource();
4651
_jsonInHtmlWriter = new JsonInHtmlWriter(writer);
4752
_isAsyncInitialized = true;
4853
}
4954

5055
private void WritePreMessage()
5156
{
52-
WriteTemplateBetween(_writer, _template, null, "{{css}}");
53-
WriteResource(_writer, "main.css");
54-
WriteTemplateBetween(_writer, _template, "{{css}}", "{{messages}}");
57+
WriteTemplateBetween(_writer, _template, null, "{{title}}");
58+
_writer.Write(_settings.Title);
59+
WriteTemplateBetween(_writer, _template, "{{title}}", "{{icon}}");
60+
_writer.Write(_settings.Icon);
61+
WriteTemplateBetween(_writer, _template, "{{icon}}", "{{css}}");
62+
_writer.Write(_settings.CssResourceLoader());
63+
WriteTemplateBetween(_writer, _template, "{{css}}", "{{custom_css}}");
64+
_writer.Write(_settings.CustomCss);
65+
WriteTemplateBetween(_writer, _template, "{{custom_css}}", "{{messages}}");
5566
}
5667

5768
private async Task WritePreMessageAsync()
5869
{
59-
await WriteTemplateBetweenAsync(_writer, _template, null, "{{css}}");
60-
await WriteResourceAsync(_writer, "main.css");
61-
await WriteTemplateBetweenAsync(_writer, _template, "{{css}}", "{{messages}}");
70+
await WriteTemplateBetweenAsync(_writer, _template, null, "{{title}}");
71+
await _writer.WriteAsync(_settings.Title);
72+
await WriteTemplateBetweenAsync(_writer, _template, "{{title}}", "{{icon}}");
73+
await _writer.WriteAsync(_settings.Icon);
74+
await WriteTemplateBetweenAsync(_writer, _template, "{{icon}}", "{{css}}");
75+
await _writer.WriteAsync(_settings.CssResourceLoader());
76+
await WriteTemplateBetweenAsync(_writer, _template, "{{css}}", "{{custom_css}}");
77+
await _writer.WriteAsync(_settings.CustomCss);
78+
await WriteTemplateBetweenAsync(_writer, _template, "{{custom_css}}", "{{messages}}");
6279
}
6380

6481
private void WritePostMessage()
6582
{
6683
WriteTemplateBetween(_writer, _template, "{{messages}}", "{{script}}");
67-
WriteResource(_writer, "main.js");
68-
WriteTemplateBetween(_writer, _template, "{{script}}", null);
84+
_writer.Write(_settings.JavascriptResourceLoader());
85+
WriteTemplateBetween(_writer, _template, "{{script}}", "{{custom_script}}");
86+
_writer.Write(_settings.CustomScript);
87+
WriteTemplateBetween(_writer, _template, "{{custom_script}}", null);
6988
}
7089

7190
private async Task WritePostMessageAsync()
7291
{
7392
await WriteTemplateBetweenAsync(_writer, _template, "{{messages}}", "{{script}}");
74-
await WriteResourceAsync(_writer, "main.js");
75-
await WriteTemplateBetweenAsync(_writer, _template, "{{script}}", null);
93+
await _writer.WriteAsync(_settings.JavascriptResourceLoader());
94+
await WriteTemplateBetweenAsync(_writer, _template, "{{script}}", "{{custom_script}}");
95+
await _writer.WriteAsync(_settings.CustomScript);
96+
await WriteTemplateBetweenAsync(_writer, _template, "{{custom_script}}", null);
7697
}
7798

7899
public void Write(Envelope envelope)
@@ -186,18 +207,6 @@ public async Task DisposeAsync()
186207
}
187208
}
188209

189-
private void WriteResource(StreamWriter writer, string v)
190-
{
191-
var resource = GetResource(v);
192-
writer.Write(resource);
193-
}
194-
195-
private async Task WriteResourceAsync(StreamWriter writer, string v)
196-
{
197-
var resource = GetResource(v);
198-
await writer.WriteAsync(resource);
199-
}
200-
201210
private void WriteTemplateBetween(StreamWriter writer, string template, string? begin, string? end)
202211
{
203212
CalculateBeginAndLength(template, begin, end, out var beginIndex, out var lengthToWrite);
@@ -217,7 +226,7 @@ private async Task WriteTemplateBetweenAsync(StreamWriter writer, string templat
217226
await writer.WriteAsync(template.Substring(beginIndex, lengthToWrite));
218227
}
219228

220-
private string GetResource(string name)
229+
internal static string GetResource(string name)
221230
{
222231
var assembly = typeof(MessagesToHtmlWriter).Assembly;
223232
var resourceStream = assembly.GetManifestResourceStream("Cucumber.HtmlFormatter.Resources." + name);
@@ -226,4 +235,9 @@ private string GetResource(string name)
226235
var resource = new StreamReader(resourceStream).ReadToEnd();
227236
return resource;
228237
}
238+
239+
private static string LoadTemplateResource()
240+
{
241+
return GetResource("index.mustache.html");
242+
}
229243
}

dotnet/Cucumber.HtmlFormatter/Resources/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
*.html
33
*.js
44
*.css
5-
*.txt
5+
*.txt
6+
*.url

dotnet/Cucumber.HtmlFormatterTest/Cucumber.HtmlFormatterTest.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<LangVersion>latest</LangVersion>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8+
<SignAssembly>true</SignAssembly>
9+
<AssemblyOriginatorKeyFile>..\Cucumber.HtmlFormatter.snk</AssemblyOriginatorKeyFile>
810
</PropertyGroup>
911

1012
<ItemGroup>

0 commit comments

Comments
 (0)