Skip to content

Commit 20afb81

Browse files
committed
Support customizations
* Allows the title and icon in the html report to replaced with a custom title and icon. * Supports either replacing or extending the default javascript and css of the report. Closes: #403
1 parent 05d0779 commit 20afb81

File tree

13 files changed

+317
-66
lines changed

13 files changed

+317
-66
lines changed

Makefile

Lines changed: 10 additions & 1 deletion
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))
@@ -18,18 +18,27 @@ clean: ## Remove javascript built module and related artifacts from java and rub
1818
ruby/assets/index.mustache.html: javascript/src/index.mustache.html
1919
cp $< $@
2020

21+
ruby/assets/icon.url: javascript/src/icon.url
22+
cp $< $@
23+
2124
ruby/assets/%: javascript/dist/%
2225
cp $< $@
2326

2427
java/src/main/resources/io/cucumber/htmlformatter/index.mustache.html: javascript/src/index.mustache.html
2528
cp $< $@
2629

30+
java/src/main/resources/io/cucumber/htmlformatter/icon.url: javascript/src/icon.url
31+
cp $< $@
32+
2733
java/src/main/resources/io/cucumber/htmlformatter/%: javascript/dist/%
2834
cp $< $@
2935

3036
dotnet/Cucumber.HtmlFormatter/Resources/index.mustache.html: javascript/src/index.mustache.html
3137
cp $< $@
3238

39+
dotnet/Cucumber.HtmlFormatter/Resources/icon.url: javascript/src/icon.url
40+
cp $< $@
41+
3342
dotnet/Cucumber.HtmlFormatter/Resources/%: javascript/dist/%
3443
cp $< $@
3544

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

java/src/main/java/io/cucumber/htmlformatter/MessagesToHtmlWriter.java

Lines changed: 215 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.io.BufferedReader;
66
import java.io.BufferedWriter;
7+
import java.io.ByteArrayInputStream;
78
import java.io.ByteArrayOutputStream;
89
import java.io.IOException;
910
import java.io.InputStream;
@@ -12,6 +13,7 @@
1213
import java.io.OutputStreamWriter;
1314
import java.io.Writer;
1415
import java.nio.charset.StandardCharsets;
16+
import java.util.function.Supplier;
1517

1618
import static java.nio.charset.StandardCharsets.UTF_8;
1719
import static java.util.Objects.requireNonNull;
@@ -20,42 +22,139 @@
2022
* Writes the message output of a test run as single page html report.
2123
*/
2224
public final class MessagesToHtmlWriter implements AutoCloseable {
23-
private final String template;
24-
private final Writer writer;
25+
private final OutputStreamWriter writer;
2526
private final JsonInHtmlWriter jsonInHtmlWriter;
2627
private final Serializer serializer;
28+
29+
private final String template;
30+
private final Supplier<InputStream> title;
31+
private final Supplier<InputStream> icon;
32+
private final Supplier<InputStream> css;
33+
private final Supplier<InputStream> customCss;
34+
private final Supplier<InputStream> script;
35+
private final Supplier<InputStream> customScript;
36+
2737
private boolean preMessageWritten = false;
2838
private boolean postMessageWritten = false;
2939
private boolean firstMessageWritten = false;
3040
private boolean streamClosed = false;
3141

42+
@Deprecated
3243
public MessagesToHtmlWriter(OutputStream outputStream, Serializer serializer) throws IOException {
3344
this(
34-
new OutputStreamWriter(
35-
requireNonNull(outputStream),
36-
StandardCharsets.UTF_8),
37-
requireNonNull(serializer)
45+
createWriter(outputStream),
46+
requireNonNull(serializer),
47+
() -> new ByteArrayInputStream("Cucumber".getBytes(UTF_8)),
48+
() -> getResource("icon.url"),
49+
() -> getResource("main.css"),
50+
MessagesToHtmlWriter::getEmptyResource,
51+
() -> getResource("main.js"),
52+
MessagesToHtmlWriter::getEmptyResource
3853
);
3954
}
4055

41-
42-
private MessagesToHtmlWriter(Writer writer, Serializer serializer) throws IOException {
56+
private MessagesToHtmlWriter(
57+
OutputStreamWriter writer,
58+
Serializer serializer,
59+
Supplier<InputStream> title,
60+
Supplier<InputStream> icon,
61+
Supplier<InputStream> css,
62+
Supplier<InputStream> customCss,
63+
Supplier<InputStream> script,
64+
Supplier<InputStream> customScript
65+
) {
4366
this.writer = writer;
4467
this.jsonInHtmlWriter = new JsonInHtmlWriter(writer);
4568
this.serializer = serializer;
46-
this.template = readResource("index.mustache.html");
69+
this.template = readTemplate();
70+
this.title = title;
71+
this.icon = icon;
72+
this.css = css;
73+
this.customCss = customCss;
74+
this.customScript = customScript;
75+
this.script = script;
76+
}
77+
78+
private static String readTemplate() {
79+
try {
80+
return readResource("index.mustache.html");
81+
} catch (IOException e) {
82+
throw new RuntimeException("Could not read resource index.mustache.html", e);
83+
}
84+
}
85+
86+
private static OutputStreamWriter createWriter(OutputStream outputStream) {
87+
return new OutputStreamWriter(
88+
requireNonNull(outputStream),
89+
StandardCharsets.UTF_8);
90+
}
91+
92+
/**
93+
* Creates a builder to construct this writer.
94+
*
95+
* @param serializer used to convert messages into json.
96+
* @return a new builder
97+
*/
98+
public static Builder builder(Serializer serializer) {
99+
return new Builder(serializer);
100+
}
101+
102+
private static ByteArrayInputStream getEmptyResource() {
103+
return new ByteArrayInputStream(new byte[0]);
104+
}
105+
106+
private static void writeTemplateBetween(Writer writer, String template, String begin, String end)
107+
throws IOException {
108+
int beginIndex = begin == null ? 0 : template.indexOf(begin) + begin.length();
109+
int endIndex = end == null ? template.length() : template.indexOf(end);
110+
writer.write(template.substring(beginIndex, endIndex));
111+
}
112+
113+
private static void writeResource(Writer writer, Supplier<InputStream> resource) throws IOException {
114+
writeResource(writer, resource.get());
115+
}
116+
117+
private static void writeResource(Writer writer, InputStream resource) throws IOException {
118+
BufferedReader reader = new BufferedReader(new InputStreamReader(resource, UTF_8));
119+
char[] buffer = new char[1024];
120+
for (int read = reader.read(buffer); read != -1; read = reader.read(buffer)) {
121+
writer.write(buffer, 0, read);
122+
}
123+
}
124+
125+
private static InputStream getResource(String name) {
126+
InputStream resource = MessagesToHtmlWriter.class.getResourceAsStream(name);
127+
requireNonNull(resource, name + " could not be loaded");
128+
return resource;
129+
}
130+
131+
private static String readResource(String name) throws IOException {
132+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
133+
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(baos, UTF_8))) {
134+
InputStream resource = getResource(name);
135+
writeResource(writer, resource);
136+
}
137+
return new String(baos.toByteArray(), UTF_8);
47138
}
48139

49140
private void writePreMessage() throws IOException {
50-
writeTemplateBetween(writer, template, null, "{{css}}");
51-
writeResource(writer, "main.css");
52-
writeTemplateBetween(writer, template, "{{css}}", "{{messages}}");
141+
writeTemplateBetween(writer, template, null, "{{title}}");
142+
writeResource(writer, title);
143+
writeTemplateBetween(writer, template, "{{title}}", "{{icon}}");
144+
writeResource(writer, icon);
145+
writeTemplateBetween(writer, template, "{{icon}}", "{{css}}");
146+
writeResource(writer, css);
147+
writeTemplateBetween(writer, template, "{{css}}", "{{customCss}}");
148+
writeResource(writer, customCss);
149+
writeTemplateBetween(writer, template, "{{customCss}}", "{{messages}}");
53150
}
54151

55152
private void writePostMessage() throws IOException {
56153
writeTemplateBetween(writer, template, "{{messages}}", "{{script}}");
57-
writeResource(writer, "main.js");
58-
writeTemplateBetween(writer, template, "{{script}}", null);
154+
writeResource(writer, script);
155+
writeTemplateBetween(writer, template, "{{script}}", "{{customScript}}");
156+
writeResource(writer, customScript);
157+
writeTemplateBetween(writer, template, "{{customScript}}", null);
59158
}
60159

61160
/**
@@ -113,31 +212,6 @@ public void close() throws IOException {
113212
}
114213
}
115214

116-
private static void writeTemplateBetween(Writer writer, String template, String begin, String end)
117-
throws IOException {
118-
int beginIndex = begin == null ? 0 : template.indexOf(begin) + begin.length();
119-
int endIndex = end == null ? template.length() : template.indexOf(end);
120-
writer.write(template.substring(beginIndex, endIndex));
121-
}
122-
123-
private static void writeResource(Writer writer, String name) throws IOException {
124-
InputStream resource = MessagesToHtmlWriter.class.getResourceAsStream(name);
125-
requireNonNull(resource, name + " could not be loaded");
126-
BufferedReader reader = new BufferedReader(new InputStreamReader(resource, UTF_8));
127-
char[] buffer = new char[1024];
128-
for (int read = reader.read(buffer); read != -1; read = reader.read(buffer)) {
129-
writer.write(buffer, 0, read);
130-
}
131-
}
132-
133-
private static String readResource(String name) throws IOException {
134-
ByteArrayOutputStream baos = new ByteArrayOutputStream();
135-
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(baos, UTF_8))) {
136-
writeResource(writer, name);
137-
}
138-
return new String(baos.toByteArray(), UTF_8);
139-
}
140-
141215
/**
142216
* Serializes a message to JSON.
143217
*/
@@ -165,4 +239,106 @@ public interface Serializer {
165239

166240
}
167241

242+
public static final class Builder {
243+
private final Serializer serializer;
244+
private Supplier<InputStream> title = () -> new ByteArrayInputStream("Cucumber".getBytes(UTF_8));
245+
private Supplier<InputStream> icon = () -> getResource("icon.url");
246+
private Supplier<InputStream> css = () -> getResource("main.css");
247+
private Supplier<InputStream> customCss = MessagesToHtmlWriter::getEmptyResource;
248+
private Supplier<InputStream> script = () -> getResource("main.js");
249+
private Supplier<InputStream> customScript = MessagesToHtmlWriter::getEmptyResource;
250+
251+
private Builder(Serializer serializer) {
252+
this.serializer = requireNonNull(serializer);
253+
}
254+
255+
/**
256+
* Sets a custom title for the report, default value "Cucumber".
257+
*
258+
* @param title the custom title.
259+
* @return this builder
260+
*/
261+
public Builder title(String title) {
262+
requireNonNull(title);
263+
this.title = () -> new ByteArrayInputStream(title.getBytes(UTF_8));
264+
return this;
265+
}
266+
267+
/**
268+
* Sets a custom icon for the report, default value the cucumber logo.
269+
* <p>
270+
* The {@code icon} is any valid {@code href} value.
271+
*
272+
* @param icon the custom icon.
273+
* @return this builder
274+
*/
275+
public Builder icon(Supplier<InputStream> icon) {
276+
this.icon = requireNonNull(icon);
277+
return this;
278+
}
279+
280+
/**
281+
* Sets default css for the report.
282+
* <p>
283+
* The default script styles the cucumber report.
284+
*
285+
* @param css the custom css.
286+
* @return this builder
287+
*/
288+
public Builder css(Supplier<InputStream> css) {
289+
this.css = requireNonNull(css);
290+
return this;
291+
}
292+
293+
/**
294+
* Sets custom css for the report.
295+
* <p>
296+
* The custom css is applied after the default css.
297+
*
298+
* @param customCss the custom css.
299+
* @return this builder
300+
*/
301+
public Builder customCss(Supplier<InputStream> customCss) {
302+
this.customCss = requireNonNull(customCss);
303+
return this;
304+
}
305+
306+
/**
307+
* Replaces default script for the report.
308+
* <p>
309+
* The default script renders the cucumber messages into a report.
310+
*
311+
* @param script the custom script.
312+
* @return this builder
313+
*/
314+
public Builder script(Supplier<InputStream> script) {
315+
this.script = requireNonNull(script);
316+
return this;
317+
}
318+
319+
/**
320+
* Sets custom script for the report.
321+
* <p>
322+
* The custom script is applied after the default script.
323+
*
324+
* @param customScript the custom script.
325+
* @return this builder
326+
*/
327+
public Builder customScript(Supplier<InputStream> customScript) {
328+
this.customScript = requireNonNull(customScript);
329+
return this;
330+
}
331+
332+
333+
/**
334+
* Create an instance of the messages to html writer.
335+
*
336+
* @param out the output stream to write to
337+
* @return a new instance of the messages to html writer.
338+
*/
339+
public MessagesToHtmlWriter build(OutputStream out) {
340+
return new MessagesToHtmlWriter(createWriter(out), serializer, title, icon, css, customCss, script, customScript);
341+
}
342+
}
343+
168344
}

java/src/main/resources/io/cucumber/htmlformatter/.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

java/src/test/java/io/cucumber/htmlformatter/Main.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public static void main(String[] args) throws IOException {
2121
in = new FileInputStream(args[0]);
2222
}
2323
try (NdjsonToMessageIterable envelopes = new NdjsonToMessageIterable(in, deserializer)) {
24-
try (MessagesToHtmlWriter htmlWriter = new MessagesToHtmlWriter(System.out, serializer)) {
24+
MessagesToHtmlWriter.Builder builder = MessagesToHtmlWriter.builder(serializer);
25+
try (MessagesToHtmlWriter htmlWriter = builder.build(System.out)) {
2526
for (Envelope envelope : envelopes) {
2627
htmlWriter.write(envelope);
2728
}

0 commit comments

Comments
 (0)