package cucumber.contrib.formatter; import gherkin.formatter.Argument; import gherkin.formatter.NiceAppendable; import gherkin.formatter.model.*; import org.pegdown.PegDownProcessor; import java.util.ArrayList; import java.util.List; import static com.google.common.base.Strings.isNullOrEmpty; import static cucumber.contrib.formatter.util.BricABrac.*; public class HtmlMarkdownReport { private final NiceAppendable appendable; private String currentUri; private FeatureWrapper currentFeature; private PegDownProcessor markdown; private int indent = 0; private boolean formatScenarioComment = true; private Statistics statistics; private boolean useChartJS = false; public HtmlMarkdownReport(NiceAppendable out) { this.appendable = out; this.markdown = new PegDownProcessor(); this.statistics = new Statistics(); startReport(); } private void startReport() { out("<!DOCTYPE html>"); out("<html lang=\"en\">"); begin("<head>"); out("<meta charset=\"utf-8\">"); out("<meta name='viewport' content='width=device-width, initial-scale=1.0'>"); out("<title>Cucumber Features</title>"); out("<link href='bootstrap/css/bootstrap.css' rel='stylesheet'>"); out("<link href='style.css' rel='stylesheet'>"); out("<script src='jquery-1.8.2.min.js'></script>"); if (useChartJS) { out("<script src='chart.min.js'></script>"); } else { out("<script src='canvasjs.min.js'></script>"); } out("<script src='formatter.js'></script>"); end("</head>"); begin("<body>"); begin("<div class='container cucumber-report'>"); out("<div class='header'></div>"); if (useChartJS) { out("<canvas id='chart' width='400' height='400'></canvas>"); } else { begin("<div class='row'>"); out("<div class='span6' id='chartScenario' style='height: 300px'></div>"); out("<div class='span6' id='chartStep' style='height: 300px'></div>"); end("</div>"); } } private void endReport() { end("</div>"); writeStatistics(); end("</body>"); end("</html>"); } private void writeStatistics() { begin("<script>"); if (useChartJS) { begin("var data=["); out("{ value: " + statistics.nbScenarioFailed + ", color: '#F7464A' },"); out("{ value: " + statistics.nbScenarioOther + ", color: '#E0E4CC' },"); out("{ value: " + statistics.nbScenarioPending + ", color: '#F38630' },"); out("{ value: " + statistics.nbScenarioSkipped + ", color: '#F38630' },"); out("{ value: " + statistics.nbScenarioSuccessed + ", color: '#69D2E7' }"); end("];"); out("var options={};"); out("var ctx = $('#chart').get(0).getContext('2d');"); out("new Chart(ctx).Pie(data, options);"); } else { begin("var chartScenario = new CanvasJS.Chart('chartScenario',"); begin("{"); out("title: {text:'Scenario Summary'},"); out("legend: {verticalAlign: 'center', horizontalAlign: 'left', fontSize: 20, fontFamily: 'Helvetica' },"); out("theme: 'theme2',"); begin("data: ["); begin("{"); out("type:'pie', "); out("indexLabelFontFamily: 'Garamond', "); out("indexLabelFontSize: 20, "); out("startAngle:-20, "); out("showInLegend: false, "); out("toolTipContent:'Scenario {legendText}: {y}',"); begin("dataPoints: ["); out("{ y: " + statistics.nbScenarioFailed + ", legendText: 'Failure', label: 'Failure: ' + " + statistics.nbScenarioFailed + "},"); out("{ y: " + statistics.nbScenarioOther + ", legendText: 'Other', label: 'Other: ' + " + statistics.nbScenarioOther + "},"); out("{ y: " + statistics.nbScenarioPending + ", legendText: 'Pending', label: 'Pending: ' + " + statistics.nbScenarioPending + "},"); out("{ y: " + statistics.nbScenarioSkipped + ", legendText: 'Skipped', label: 'Skipped: ' + " + statistics.nbScenarioSkipped + "},"); out("{ y: " + statistics.nbScenarioSuccessed + ", legendText: 'Success', label: 'Success: ' + " + statistics.nbScenarioSuccessed + "}"); end("]"); end("}"); end("]"); end("});"); out("chartScenario.render();"); begin("var chartSteps = new CanvasJS.Chart('chartStep',"); begin("{"); out("title: {text:'Steps Summary'},"); out("legend: {verticalAlign: 'center', horizontalAlign: 'left', fontSize: 20, fontFamily: 'Helvetica' },"); out("theme: 'theme1',"); begin("data: ["); begin("{"); out("type:'pie', "); out("indexLabelFontFamily: 'Garamond', "); out("indexLabelFontSize: 20, "); out("startAngle:-20, "); out("showInLegend: false, "); out("toolTipContent:'Step {legendText}: {y}',"); begin("dataPoints: ["); out("{ y: " + statistics.nbStepFailed + ", legendText: 'Failure', label: 'Failure: ' + " + statistics.nbStepFailed + " },"); out("{ y: " + statistics.nbStepOther + ", legendText: 'Other', label: 'Other: ' + " + statistics.nbStepOther + " },"); out("{ y: " + statistics.nbStepPending + ", legendText: 'Pending', label: 'Pending: ' + " + statistics.nbStepPending + " },"); out("{ y: " + statistics.nbStepSkipped + ", legendText: 'Skipped', label: 'Skipped: ' + " + statistics.nbStepSkipped + " },"); out("{ y: " + statistics.nbStepSuccessed + ", legendText: 'Success', label: 'Success: ' + " + statistics.nbStepSuccessed + " },"); out("{ y: " + statistics.nbStepNoMatching + ", legendText: 'No match', label: 'No match: ' + " + statistics.nbStepNoMatching + " }"); end("]"); end("}"); end("]"); end("});"); out("chartSteps.render();"); } end("</script>"); } private void out(String line) { indent().append(line).append(NL); } private void begin(String line) { out(line); indent++; } private void end(String line) { indent--; out(line); } private NiceAppendable indent() { for (int i = 0; i < indent; i++) { appendable.append(" "); } return appendable; } public void uri(String uri) { currentUri = uri; } public void feature(Feature feature) { flushCurrentFeature(); this.currentFeature = new FeatureWrapper(currentUri, feature); } private void flushCurrentFeature() { if (currentFeature == null) { return; } currentFeature.consolidate(statistics); currentFeature.emit(this); currentFeature = null; } public void background(Background background) { this.currentFeature.background(background); } public void scenario(Scenario scenario) { currentFeature.scenario(scenario); } public void scenarioOutline(ScenarioOutline scenarioOutline) { throw new UnsupportedOperationException(); } public void examples(Examples examples) { throw new UnsupportedOperationException(); } public void step(Step step) { currentFeature.step(step); } public void match(Match match) { currentFeature.match(match); } public void result(Result result) { currentFeature.result(result); } public void done() { flushCurrentFeature(); endReport(); } public void eof() { } public interface Wrapper { void consolidate(Statistics statistics); void emit(HtmlMarkdownReport out); } public class FeatureWrapper implements Wrapper { private final Feature feature; private final String uri; private BackgroundWrapper background; private List<ScenarioWrapper> scenarios = new ArrayList<ScenarioWrapper>(); public FeatureWrapper(String uri, Feature feature) { this.uri = uri; this.feature = feature; } public void background(Background background) { this.background = new BackgroundWrapper(background); } public void result(Result result) { currentStepContainer().result(result); } public void match(Match match) { currentStepContainer().match(match); } public void step(Step step) { currentStepContainer().step(step); } public void scenario(Scenario scenario) { ScenarioWrapper wrapper = new ScenarioWrapper(scenario); if (this.background != null) { wrapper.setBackground(background); this.background = null; } this.scenarios.add(wrapper); } private StepContainer currentStepContainer() { return (background != null) ? background : currentScenario(); } private ScenarioWrapper currentScenario() { return this.scenarios.get(this.scenarios.size() - 1); } private String featureNameToAnchor(String name) { return "feature-" + name.toLowerCase().replace(' ', '_'); } @Override public void emit(HtmlMarkdownReport r) { r.begin("<section class='feature'>"); String featureName = feature.getName(); r.out("<h1 id='" + featureNameToAnchor(featureName) + "'>" + featureName + "</h1>"); r.out("<p class='uri'>" + uri + "</p>"); r.begin("<p class='description'>"); r.out(formatHtml(feature.getDescription())); r.end("</p>"); r.begin("<div class='scenario-list'>"); for (ScenarioWrapper scenario : scenarios) { scenario.emit(r); } r.end("</div>"); r.end("</section>"); } @Override public void consolidate(Statistics statistics) { statistics.feature(); for (ScenarioWrapper scenario : scenarios) { scenario.consolidate(statistics); } } } protected class StepContainer { protected List<StepWrapper> steps = new ArrayList<StepWrapper>(); private int stepCursor = -1; // used by match & result public void match(Match match) { matchStep().match(match); } public void result(Result result) { resultStep().result(result); } private StepWrapper matchStep() { return this.steps.get(++stepCursor); // match is triggered before // result } private StepWrapper resultStep() { return this.steps.get(stepCursor); } public void step(Step step) { steps.add(new StepWrapper(step)); } } public class BackgroundWrapper extends StepContainer implements Wrapper { public BackgroundWrapper(Background background) { } @Override public void emit(HtmlMarkdownReport out) { } @Override public void consolidate(Statistics statistics) { } } public class ScenarioWrapper extends StepContainer implements Wrapper { private final Scenario scenario; private BackgroundWrapper background; public ScenarioWrapper(Scenario scenario) { this.scenario = scenario; } public void setBackground(BackgroundWrapper background) { this.background = background; } private String scenarioNameToAnchor(String name) { return "scenario-" + name.toLowerCase().replace(' ', '_'); } @Override public void consolidate(Statistics statistics) { for (StepWrapper step : steps) { step.consolidate(statistics); } for (StepWrapper step : steps) { if (step.isFailure()) { statistics.scenarioFailed(); return; } else if (step.isSkipped()) { statistics.scenarioSkipped(); return; } else if (step.isPending()) { statistics.scenarioPending(); return; } else if (!step.isSuccess()) { statistics.scenarioOther(); return; } } statistics.scenarioSuccessed(); } @Override public void emit(HtmlMarkdownReport r) { r.begin("<section class='scenario'>"); String extra = ""; if (!scenario.getKeyword().equalsIgnoreCase("Scenario")) { extra = " <small>(" + scenario.getKeyword() + ")</small>"; } r.out("<h2 id='" + scenarioNameToAnchor(scenario.getName()) + "'>" + scenario.getName() + extra + "</h2>"); r.begin("<div class='tags'>"); for (Tag tag : scenario.getTags()) { r.out("<span class='tag'>" + tag.getName() + " </span>"); } r.end("</div>"); r.begin("<p class='description'>"); r.out(formatHtml(scenario.getDescription())); r.end("</p>"); if (formatScenarioComment && !steps.isEmpty()) { List<Comment> comments = steps.get(0).step.getComments(); StringBuilder commentsInlined = new StringBuilder(); for (Comment comment : comments) { commentsInlined.append(discardCommentChar(comment.getValue())).append(NL); } r.begin("<p class='comments'>"); String formatted = formatHtml(commentsInlined.toString()); r.out(formatted); r.end("</p>"); } r.begin("<ol class='step-list'>"); for (StepWrapper step : steps) step.emit(r); r.end("</ol>"); r.end("</section>"); } } public class StepWrapper implements Wrapper { private final Step step; private Result result; private Match match; public StepWrapper(Step step) { this.step = step; } public boolean isOutlined() { return !isEmpty(step.getOutlineArgs()); } public void result(Result result) { this.result = result; } public void match(Match match) { this.match = match; } public boolean isMatching() { return !isNullOrEmpty(match.getLocation()); } public boolean isSuccess() { return areEqualsIgnoringCase("passed", result.getStatus()); } public boolean isSkipped() { return areEqualsIgnoringCase("skipped", result.getStatus()); } public boolean isPending() { return areEqualsIgnoringCase("pending", result.getStatus()); } public boolean isFailure() { return areEqualsIgnoringCase("failed", result.getStatus()); } @Override public void consolidate(Statistics statistics) { if (!isMatching()) { statistics.stepNoMatching(); } else if (isFailure()) { statistics.stepFailed(); } else if (isSkipped()) { statistics.stepSkipped(); } else if (isPending()) { statistics.stepPending(); } else if (isSuccess()) { statistics.stepSuccessed(); } else { statistics.stepOther(); } } @Override public void emit(HtmlMarkdownReport r) { String cssStyles = "step"; if (!isMatching()) cssStyles = " step-unmatch"; r.begin("<li class='" + cssStyles + "'>"); r.out("<span class='step-status " + result.getStatus() + "'></span>"); r.out("<span class='step-keyword'>" + step.getKeyword() + "</span> "); r.out("<span class='step-text'>" + step.getName() + "</span>"); if (!isEmpty(step.getOutlineArgs())) { StringBuilder builder = new StringBuilder(); for (Argument arg : step.getOutlineArgs()) { if (builder.length() > 0) { builder.append(", "); } builder.append(arg.getVal()); } r.out("<span class='outline-arguments'>(" + builder + ")</span>"); } if (!isEmpty(step.getRows())) { boolean firstRow = true; r.begin("<div class='parameters'>"); r.begin("<table class='data-table'>"); for (Row row : step.getRows()) { String cellPrefix = "<td>"; String cellSuffix = "</td>"; if (firstRow) { r.begin("<thead>"); cellPrefix = "<th>"; cellSuffix = "</th>"; } r.begin("<tr>"); for (String cell : row.getCells()) { r.out(cellPrefix + cell + cellSuffix); } r.end("</tr>"); if (firstRow) { r.end("</thead>"); r.begin("<tbody>"); firstRow = false; } } r.end("</tbody>"); r.end("</table>"); r.end("</div>"); } r.end("</li>"); } } public String formatHtml(String text) { if (isNullOrEmpty(text)) { return ""; } return markdown.markdownToHtml(text); } public static class Statistics { private int nbScenarioSuccessed; private int nbScenarioSkipped; private int nbScenarioFailed; private int nbScenarioOther; private int nbScenarioPending; private int nbStepSuccessed; private int nbFeature; private int nbStepOther; private int nbStepPending; private int nbStepSkipped; private int nbStepFailed; private int nbStepNoMatching; public void feature() { nbFeature++; } public void stepOther() { nbStepOther++; } public void stepPending() { nbStepPending++; } public void stepSkipped() { nbStepSkipped++; } public void stepFailed() { nbStepFailed++; } public void stepNoMatching() { nbStepNoMatching++; } public void stepSuccessed() { nbStepSuccessed++; } public void scenarioOther() { nbScenarioOther++; } public void scenarioPending() { nbScenarioPending++; } public void scenarioFailed() { nbScenarioFailed++; } public void scenarioSuccessed() { nbScenarioSuccessed++; } public void scenarioSkipped() { nbScenarioSkipped++; } } }