diff --git a/core/src/main/java/cucumber/runtime/RuntimeOptions.java b/core/src/main/java/cucumber/runtime/RuntimeOptions.java index 72ef362e3b..affb1bd68e 100644 --- a/core/src/main/java/cucumber/runtime/RuntimeOptions.java +++ b/core/src/main/java/cucumber/runtime/RuntimeOptions.java @@ -304,10 +304,6 @@ public List cucumberFeatures(ResourceLoader resourceLoader, Eve public List getPlugins() { if (!pluginNamesInstantiated) { for (String pluginName : pluginFormatterNames) { - if (notUpdatedFormatter(pluginName)) { - System.out.println("WARNING: The " + pluginName.split(":")[0] + " formatter is not updated yet and is therefore not used."); - continue; - } Object plugin = pluginFactory.create(pluginName); plugins.add(plugin); setMonochromeOnColorAwarePlugins(plugin); @@ -327,16 +323,6 @@ public List getPlugins() { return plugins; } - private boolean notUpdatedFormatter(String pluginName) { - List NOT_UPDATED_FORMATTERS = java.util.Arrays.asList("html"); - for (String name : NOT_UPDATED_FORMATTERS) { - if (name.equals(pluginName.split(":")[0])) { - return true; - } - } - return false; - } - public Formatter formatter(ClassLoader classLoader) { return pluginProxy(classLoader, Formatter.class); } diff --git a/core/src/main/java/cucumber/runtime/formatter/HTMLFormatter.java b/core/src/main/java/cucumber/runtime/formatter/HTMLFormatter.java index 8e11309a02..d1fad97c0f 100644 --- a/core/src/main/java/cucumber/runtime/formatter/HTMLFormatter.java +++ b/core/src/main/java/cucumber/runtime/formatter/HTMLFormatter.java @@ -1,12 +1,41 @@ package cucumber.runtime.formatter; +import cucumber.api.Result; +import cucumber.api.TestCase; +import cucumber.api.TestStep; +import cucumber.api.event.EmbedEvent; +import cucumber.api.event.EventHandler; import cucumber.api.event.EventPublisher; +import cucumber.api.event.TestCaseStarted; +import cucumber.api.event.TestRunFinished; +import cucumber.api.event.TestSourceRead; +import cucumber.api.event.TestStepFinished; +import cucumber.api.event.TestStepStarted; +import cucumber.api.event.WriteEvent; import cucumber.api.formatter.Formatter; import cucumber.api.formatter.NiceAppendable; import cucumber.runtime.CucumberException; import cucumber.runtime.io.URLOutputStream; +import gherkin.ast.Background; +import gherkin.ast.DataTable; +import gherkin.ast.DocString; +import gherkin.ast.Examples; +import gherkin.ast.Feature; +import gherkin.ast.Node; +import gherkin.ast.ScenarioDefinition; +import gherkin.ast.ScenarioOutline; +import gherkin.ast.Step; +import gherkin.ast.TableCell; +import gherkin.ast.TableRow; +import gherkin.ast.Tag; import gherkin.deps.com.google.gson.Gson; import gherkin.deps.com.google.gson.GsonBuilder; +import gherkin.pickles.Argument; +import gherkin.pickles.PickleCell; +import gherkin.pickles.PickleRow; +import gherkin.pickles.PickleString; +import gherkin.pickles.PickleTable; +import gherkin.pickles.PickleTag; import java.io.File; import java.io.IOException; @@ -14,6 +43,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.URL; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,122 +64,381 @@ class HTMLFormatter implements Formatter { } }; + private final TestSourcesModel testSources = new TestSourcesModel(); private final URL htmlReportDir; private NiceAppendable jsOut; private boolean firstFeature = true; + private String currentFeatureFile; + private Map currentTestCaseMap; + private ScenarioOutline currentScenarioOutline; + private Examples currentExamples; private int embeddedIndex; + private EventHandler testSourceReadHandler = new EventHandler() { + @Override + public void receive(TestSourceRead event) { + handleTestSourceRead(event); + } + }; + private EventHandler caseStartedHandler= new EventHandler() { + @Override + public void receive(TestCaseStarted event) { + handleTestCaseStarted(event); + } + }; + private EventHandler stepStartedHandler = new EventHandler() { + @Override + public void receive(TestStepStarted event) { + handleTestStepStarted(event); + } + }; + private EventHandler stepFinishedHandler = new EventHandler() { + @Override + public void receive(TestStepFinished event) { + handleTestStepFinished(event); + } + }; + private EventHandler embedEventhandler = new EventHandler() { + @Override + public void receive(EmbedEvent event) { + handleEmbed(event); + } + }; + private EventHandler writeEventhandler = new EventHandler() { + @Override + public void receive(WriteEvent event) { + handleWrite(event); + } + }; + private EventHandler runFinishedHandler = new EventHandler() { + @Override + public void receive(TestRunFinished event) { + finishReport(); + } + }; + public HTMLFormatter(URL htmlReportDir) { this.htmlReportDir = htmlReportDir; } + HTMLFormatter(URL htmlReportDir, NiceAppendable jsOut) { + this(htmlReportDir); + this.jsOut = jsOut; + } + @Override public void setEventPublisher(EventPublisher publisher) { + publisher.registerHandlerFor(TestSourceRead.class, testSourceReadHandler); + publisher.registerHandlerFor(TestCaseStarted.class, caseStartedHandler); + publisher.registerHandlerFor(TestStepStarted.class, stepStartedHandler); + publisher.registerHandlerFor(TestStepFinished.class, stepFinishedHandler); + publisher.registerHandlerFor(EmbedEvent.class, embedEventhandler); + publisher.registerHandlerFor(WriteEvent.class, writeEventhandler); + publisher.registerHandlerFor(TestRunFinished.class, runFinishedHandler); } -// @Override -// public void uri(String uri) { -// if (firstFeature) { -// jsOut().append("$(document).ready(function() {").append("var ") -// .append(JS_FORMATTER_VAR).append(" = new CucumberHTML.DOMFormatter($('.cucumber-report'));"); -// firstFeature = false; -// } -// jsFunctionCall("uri", uri); -// } + private void handleTestSourceRead(TestSourceRead event) { + testSources.addSource(event.path, event.source); + } -/* @Override - public void feature(Feature feature) { - jsFunctionCall("feature", feature); + private void handleTestCaseStarted(TestCaseStarted event) { + if (firstFeature) { + jsOut().append("$(document).ready(function() {").append("var ") + .append(JS_FORMATTER_VAR).append(" = new CucumberHTML.DOMFormatter($('.cucumber-report'));"); + firstFeature = false; + } + handleStartOfFeature(event.testCase); + handleScenarioOutline(event.testCase); + currentTestCaseMap = createTestCase(event.testCase); + if (testSources.hasBackground(currentFeatureFile, event.testCase.getLine())) { + jsFunctionCall("background", createBackground(event.testCase)); + } else { + jsFunctionCall("scenario", currentTestCaseMap); + currentTestCaseMap = null; + } } - @Override - public void background(Background background) { - jsFunctionCall("background", background); + private void handleTestStepStarted(TestStepStarted event) { + if (!event.testStep.isHook()) { + if (isFirstStepAfterBackground(event.testStep)) { + jsFunctionCall("scenario", currentTestCaseMap); + currentTestCaseMap = null; + } + jsFunctionCall("step", createTestStep(event.testStep)); + } } - @Override - public void scenario(Scenario scenario) { - jsFunctionCall("scenario", scenario); + private void handleTestStepFinished(TestStepFinished event) { + if (!event.testStep.isHook()) { + jsFunctionCall("match", createMatchMap(event.testStep, event.result)); + jsFunctionCall("result", createResultMap(event.result)); + } else { + jsFunctionCall(event.testStep.getHookType().toString(), createResultMap(event.result)); + } } - @Override - public void scenarioOutline(ScenarioOutline scenarioOutline) { - jsFunctionCall("scenarioOutline", scenarioOutline); + private void handleEmbed(EmbedEvent event) { + String mimeType = event.mimeType; + if(mimeType.startsWith("text/")) { + // just pass straight to the formatter to output in the html + jsFunctionCall("embedding", mimeType, new String(event.data)); + } else { + // Creating a file instead of using data urls to not clutter the js file + String extension = MIME_TYPES_EXTENSIONS.get(mimeType); + if (extension != null) { + StringBuilder fileName = new StringBuilder("embedded").append(embeddedIndex++).append(".").append(extension); + writeBytesAndClose(event.data, reportFileOutputStream(fileName.toString())); + jsFunctionCall("embedding", mimeType, fileName); + } + } } - @Override - public void examples(Examples examples) { - jsFunctionCall("examples", examples); + private void handleWrite(WriteEvent event) { + jsFunctionCall("write", event.text); } - @Override - public void step(Step step) { - jsFunctionCall("step", step); - }*/ - -// @Override -// public void done() { -// if (!firstFeature) { -// jsOut().append("});"); -// copyReportFiles(); -// } -// } -// - public void close() { + private void finishReport() { + if (!firstFeature) { + jsOut().append("});"); + copyReportFiles(); + } jsOut().close(); } -/* @Override - public void startOfScenarioLifeCycle(Scenario scenario) { - // NoOp + private void handleStartOfFeature(TestCase testCase) { + if (currentFeatureFile == null || !currentFeatureFile.equals(testCase.getPath())) { + currentFeatureFile = testCase.getPath(); + jsFunctionCall("uri", currentFeatureFile); + jsFunctionCall("feature", createFeature(testCase)); + } } - @Override - public void endOfScenarioLifeCycle(Scenario scenario) { - // NoOp + private Map createFeature(TestCase testCase) { + Map featureMap = new HashMap(); + Feature feature = testSources.getFeature(testCase.getPath()); + if (feature != null) { + featureMap.put("keyword", feature.getKeyword()); + featureMap.put("name", feature.getName()); + featureMap.put("description", feature.getDescription() != null ? feature.getDescription() : ""); + if (!feature.getTags().isEmpty()) { + featureMap.put("tags", createTagList(feature.getTags())); + } + } + return featureMap; } - @Override - public void result(Result result) { - jsFunctionCall("result", result); + private List> createTagList(List tags) { + List> tagList = new ArrayList>(); + for (Tag tag : tags) { + Map tagMap = new HashMap(); + tagMap.put("name", tag.getName()); + tagList.add(tagMap); + } + return tagList; } - @Override - public void before(Match match, Result result) { - jsFunctionCall("before", result); + private void handleScenarioOutline(TestCase testCase) { + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testCase.getLine()); + if (TestSourcesModel.isScenarioOutlineScenario(astNode)) { + ScenarioOutline scenarioOutline = (ScenarioOutline)TestSourcesModel.getScenarioDefinition(astNode); + if (currentScenarioOutline == null || !currentScenarioOutline.equals(scenarioOutline)) { + currentScenarioOutline = scenarioOutline; + jsFunctionCall("scenarioOutline", createScenarioOutline(currentScenarioOutline)); + addOutlineStepsToReport(scenarioOutline); + } + Examples examples = (Examples)astNode.parent.node; + if (currentExamples == null || !currentExamples.equals(examples)) { + currentExamples = examples; + jsFunctionCall("examples", createExamples(currentExamples)); + } + } else { + currentScenarioOutline = null; + currentExamples = null; + } } - @Override - public void after(Match match, Result result) { - jsFunctionCall("after", result); + private Map createScenarioOutline(ScenarioOutline scenarioOutline) { + Map scenarioOutlineMap = new HashMap(); + scenarioOutlineMap.put("name", scenarioOutline.getName()); + scenarioOutlineMap.put("keyword", scenarioOutline.getKeyword()); + scenarioOutlineMap.put("description", scenarioOutline.getDescription() != null ? scenarioOutline.getDescription() : ""); + if (!scenarioOutline.getTags().isEmpty()) { + scenarioOutlineMap.put("tags", createTagList(scenarioOutline.getTags())); + } + return scenarioOutlineMap; } - @Override - public void match(Match match) { - jsFunctionCall("match", match); - }*/ - -// @Override -// public void embedding(String mimeType, byte[] data) { -// if(mimeType.startsWith("text/")) { -// // just pass straight to the formatter to output in the html -// jsFunctionCall("embedding", mimeType, new String(data)); -// } else { -// // Creating a file instead of using data urls to not clutter the js file -// String extension = MIME_TYPES_EXTENSIONS.get(mimeType); -// if (extension != null) { -// StringBuilder fileName = new StringBuilder("embedded").append(embeddedIndex++).append(".").append(extension); -// writeBytesAndClose(data, reportFileOutputStream(fileName.toString())); -// jsFunctionCall("embedding", mimeType, fileName); -// } -// } -// } - -// @Override -// public void write(String text) { -// jsFunctionCall("write", text); -// } + private void addOutlineStepsToReport(ScenarioOutline scenarioOutline) { + for (Step step : scenarioOutline.getSteps()) { + Map stepMap = new HashMap(); + stepMap.put("name", step.getText()); + stepMap.put("keyword", step.getKeyword()); + if (step.getArgument() != null) { + Node argument = step.getArgument(); + if (argument instanceof DocString) { + stepMap.put("doc_string", createDocStringMap((DocString)argument)); + } else if (argument instanceof DataTable) { + stepMap.put("rows", createDataTableList((DataTable)argument)); + } + } + jsFunctionCall("step", stepMap); + } + } + + private Map createDocStringMap(DocString docString) { + Map docStringMap = new HashMap(); + docStringMap.put("value", docString.getContent()); + return docStringMap; + } + + private List> createDataTableList(DataTable dataTable) { + List> rowList = new ArrayList>(); + for (TableRow row : dataTable.getRows()) { + rowList.add(createRowMap(row)); + } + return rowList; + } + + private Map createRowMap(TableRow row) { + Map rowMap = new HashMap(); + rowMap.put("cells", createCellList(row)); + return rowMap; + } + + private List createCellList(TableRow row) { + List cells = new ArrayList(); + for (TableCell cell : row.getCells()) { + cells.add(cell.getValue()); + } + return cells; + } + + private Map createExamples(Examples examples) { + Map examplesMap = new HashMap(); + examplesMap.put("name", examples.getName()); + examplesMap.put("keyword", examples.getKeyword()); + examplesMap.put("description", examples.getDescription() != null ? examples.getDescription() : ""); + List> rowList = new ArrayList>(); + rowList.add(createRowMap(examples.getTableHeader())); + for (TableRow row : examples.getTableBody()) { + rowList.add(createRowMap(row)); + } + examplesMap.put("rows", rowList); + if (!examples.getTags().isEmpty()) { + examplesMap.put("tags", createTagList(examples.getTags())); + } + return examplesMap; + } + + private Map createTestCase(TestCase testCase) { + Map testCaseMap = new HashMap(); + testCaseMap.put("name", testCase.getName()); + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testCase.getLine()); + if (astNode != null) { + ScenarioDefinition scenarioDefinition = TestSourcesModel.getScenarioDefinition(astNode); + testCaseMap.put("keyword", scenarioDefinition.getKeyword()); + testCaseMap.put("description", scenarioDefinition.getDescription() != null ? scenarioDefinition.getDescription() : ""); + } + if (!testCase.getTags().isEmpty()) { + List> tagList = new ArrayList>(); + for (PickleTag tag : testCase.getTags()) { + Map tagMap = new HashMap(); + tagMap.put("name", tag.getName()); + tagList.add(tagMap); + } + testCaseMap.put("tags", tagList); + } + return testCaseMap; + } + + private Map createBackground(TestCase testCase) { + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testCase.getLine()); + if (astNode != null) { + Background background = TestSourcesModel.getBackgoundForTestCase(astNode); + Map testCaseMap = new HashMap(); + testCaseMap.put("name", background.getName()); + testCaseMap.put("keyword", background.getKeyword()); + testCaseMap.put("description", background.getDescription() != null ? background.getDescription() : ""); + return testCaseMap; + } + return null; + } + + private boolean isFirstStepAfterBackground(TestStep testStep) { + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testStep.getStepLine()); + if (astNode != null) { + if (currentTestCaseMap != null && !TestSourcesModel.isBackgroundStep(astNode)) { + return true; + } + } + return false; + } + + private Map createTestStep(TestStep testStep) { + Map stepMap = new HashMap(); + stepMap.put("name", testStep.getStepText()); + if (!testStep.getStepArgument().isEmpty()) { + Argument argument = testStep.getStepArgument().get(0); + if (argument instanceof PickleString) { + stepMap.put("doc_string", createDocStringMap((PickleString)argument)); + } else if (argument instanceof PickleTable) { + stepMap.put("rows", createDataTableList((PickleTable)argument)); + } + } + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testStep.getStepLine()); + if (astNode != null) { + Step step = (Step) astNode.node; + stepMap.put("keyword", step.getKeyword()); + } + + return stepMap; + } + + private Map createDocStringMap(PickleString docString) { + Map docStringMap = new HashMap(); + docStringMap.put("value", docString.getContent()); + return docStringMap; + } + + private List> createDataTableList(PickleTable dataTable) { + List> rowList = new ArrayList>(); + for (PickleRow row : dataTable.getRows()) { + rowList.add(createRowMap(row)); + } + return rowList; + } + + private Map createRowMap(PickleRow row) { + Map rowMap = new HashMap(); + rowMap.put("cells", createCellList(row)); + return rowMap; + } + + private List createCellList(PickleRow row) { + List cells = new ArrayList(); + for (PickleCell cell : row.getCells()) { + cells.add(cell.getValue()); + } + return cells; + } + + private Map createMatchMap(TestStep testStep, Result result) { + Map matchMap = new HashMap(); + if (!result.getStatus().equals(Result.UNDEFINED)) { + matchMap.put("location", testStep.getCodeLocation()); + } + return matchMap; + } + + private Map createResultMap(Result result) { + Map resultMap = new HashMap(); + resultMap.put("status", result.getStatus()); + if (result.getErrorMessage() != null) { + resultMap.put("error_message", result.getErrorMessage()); + } + return resultMap; + } private void jsFunctionCall(String functionName, Object... args) { NiceAppendable out = jsOut().append(JS_FORMATTER_VAR + ".").append(functionName).append("("); @@ -158,7 +447,6 @@ private void jsFunctionCall(String functionName, Object... args) { if (comma) { out.append(", "); } - //arg = arg instanceof Mappable ? ((Mappable) arg).toMap() : arg; String stringArg = gson.toJson(arg); out.append(stringArg); comma = true; @@ -167,6 +455,9 @@ private void jsFunctionCall(String functionName, Object... args) { } private void copyReportFiles() { + if (htmlReportDir == null) { + return; + } for (String textAsset : TEXT_ASSETS) { InputStream textAssetStream = getClass().getResourceAsStream(textAsset); if (textAssetStream == null) { diff --git a/core/src/main/java/cucumber/runtime/formatter/TestSourcesModel.java b/core/src/main/java/cucumber/runtime/formatter/TestSourcesModel.java index 6c37e9c519..30893fed62 100644 --- a/core/src/main/java/cucumber/runtime/formatter/TestSourcesModel.java +++ b/core/src/main/java/cucumber/runtime/formatter/TestSourcesModel.java @@ -59,6 +59,9 @@ public static String calculateId(AstNode astNode) { if (node instanceof ExamplesRowWrapperNode) { return calculateId(astNode.parent) + ";" + Integer.toString(((ExamplesRowWrapperNode)node).bodyRowIndex + 2); } + if (node instanceof TableRow) { + return calculateId(astNode.parent) + ";" + Integer.toString(1); + } if (node instanceof Examples) { return calculateId(astNode.parent) + ";" + convertToId(((Examples)node).getName()); } @@ -145,6 +148,9 @@ private void processScenarioDefinition(Map nodeMap, ScenarioDe private void processScenarioOutlineExamples(Map nodeMap, ScenarioOutline scenarioOutline, AstNode childNode) { for (Examples examples : scenarioOutline.getExamples()) { AstNode examplesNode = new AstNode(examples, childNode); + TableRow headerRow = examples.getTableHeader(); + AstNode headerNode = new AstNode(headerRow, examplesNode); + nodeMap.put(headerRow.getLocation().getLine(), headerNode); for (int i = 0; i < examples.getTableBody().size(); ++i) { TableRow examplesRow = examples.getTableBody().get(i); Node rowNode = new ExamplesRowWrapperNode(examplesRow, i); diff --git a/core/src/test/java/cucumber/runtime/formatter/HTMLFormatterTest.java b/core/src/test/java/cucumber/runtime/formatter/HTMLFormatterTest.java index fe6e27f773..6d165d60d5 100644 --- a/core/src/test/java/cucumber/runtime/formatter/HTMLFormatterTest.java +++ b/core/src/test/java/cucumber/runtime/formatter/HTMLFormatterTest.java @@ -1,46 +1,66 @@ package cucumber.runtime.formatter; +import cucumber.api.Result; +import cucumber.api.formatter.NiceAppendable; +import cucumber.runtime.TestHelper; import cucumber.runtime.Utils; +import cucumber.runtime.model.CucumberFeature; import cucumber.util.FixJava; +import gherkin.deps.com.google.gson.JsonParser; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; -import org.junit.Before; import org.junit.Test; +import org.mockito.stubbing.Answer; import org.mozilla.javascript.Context; import org.mozilla.javascript.EcmaError; import org.mozilla.javascript.tools.shell.Global; import java.io.File; -import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import static cucumber.runtime.TestHelper.createEmbedHookAction; +import static cucumber.runtime.TestHelper.createWriteHookAction; +import static cucumber.runtime.TestHelper.feature; +import static cucumber.runtime.TestHelper.result; +import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class HTMLFormatterTest { + private final static String jsFunctionCallRegexString = "formatter.(\\w*)\\(([^)]*)\\);"; + private final static Pattern jsFunctionCallRegex = Pattern.compile(jsFunctionCallRegexString); private URL outputDir; - @Before - public void writeReport() throws IOException { + public void writeReport() throws Throwable { outputDir = Utils.toURL(TempDir.createTempDirectory().getAbsolutePath()); runFeaturesWithFormatter(outputDir); } - @Test @org.junit.Ignore - public void writes_index_html() throws IOException { + @Test + public void writes_index_html() throws Throwable { + writeReport(); URL indexHtml = new URL(outputDir, "index.html"); Document document = Jsoup.parse(new File(indexHtml.getFile()), "UTF-8"); Element reportElement = document.body().getElementsByClass("cucumber-report").first(); assertEquals("", reportElement.text()); } - @Test @org.junit.Ignore - public void writes_valid_report_js() throws IOException { + @Test + public void writes_valid_report_js() throws Throwable { + writeReport(); URL reportJs = new URL(outputDir, "report.js"); Context cx = Context.enter(); Global scope = new Global(cx); @@ -52,32 +72,568 @@ public void writes_valid_report_js() throws IOException { } } - @Test @org.junit.Ignore - public void includes_uri() throws IOException { + @Test + public void includes_uri() throws Throwable { + writeReport(); String reportJs = FixJava.readReader(new InputStreamReader(new URL(outputDir, "report.js").openStream(), "UTF-8")); assertContains("formatter.uri(\"some\\\\windows\\\\path\\\\some.feature\");", reportJs); } - @Test @org.junit.Ignore - public void included_embedding() throws IOException { + @Test + public void included_embedding() throws Throwable { + writeReport(); String reportJs = FixJava.readReader(new InputStreamReader(new URL(outputDir, "report.js").openStream(), "UTF-8")); assertContains("formatter.embedding(\"image/png\", \"embedded0.png\");", reportJs); assertContains("formatter.embedding(\"text/plain\", \"dodgy stack trace here\");", reportJs); } + @Test + public void should_handle_a_single_scenario() throws Throwable { + CucumberFeature feature = feature("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n" + + " Then second step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", result("passed")); + stepsToResult.put("second step", result("passed")); + Map stepsToLocation = new HashMap(); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + stepsToLocation.put("second step", "path/step_definitions.java:7"); + long stepDuration = 1; + + String formatterOutput = runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, stepDuration); + + assertJsFunctionCallSequence(asList("" + + "formatter.uri(\"path/test.feature\");\n", "" + + "formatter.feature({\n" + + " \"description\": \"\",\n" + + " \"name\": \"feature name\",\n" + + " \"keyword\": \"Feature\"\n" + + "});\n", "" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario\",\n" + + " \"name\": \"scenario name\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Then \",\n" + + " \"name\": \"second step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:7\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});"), + formatterOutput); + } + + @Test + public void should_handle_backgound() throws Throwable { + CucumberFeature feature = feature("path/test.feature", "" + + "Feature: feature name\n" + + " Background: background name\n" + + " Given first step\n" + + " Scenario: scenario 1\n" + + " Then second step\n" + + " Scenario: scenario 2\n" + + " Then third step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", result("passed")); + stepsToResult.put("second step", result("passed")); + stepsToResult.put("third step", result("passed")); + Map stepsToLocation = new HashMap(); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + stepsToLocation.put("second step", "path/step_definitions.java:7"); + stepsToLocation.put("third step", "path/step_definitions.java:11"); + long stepDuration = 1; + + String formatterOutput = runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, stepDuration); + + assertJsFunctionCallSequence(asList("" + + "formatter.background({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Background\",\n" + + " \"name\": \"background name\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario\",\n" + + " \"name\": \"scenario 1\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Then \",\n" + + " \"name\": \"second step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:7\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.background({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Background\",\n" + + " \"name\": \"background name\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario\",\n" + + " \"name\": \"scenario 2\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Then \",\n" + + " \"name\": \"third step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:11\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n"), + formatterOutput); + } + + @Test + public void should_handle_scenario_outline() throws Throwable { + CucumberFeature feature = feature("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario Outline: outline name\n" + + " Given first step\n" + + " Then step\n" + + " Examples: examples name\n" + + " | arg |\n" + + " | second |\n" + + " | third |\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", result("passed")); + stepsToResult.put("second step", result("passed")); + stepsToResult.put("third step", result("passed")); + Map stepsToLocation = new HashMap(); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + stepsToLocation.put("second step", "path/step_definitions.java:7"); + stepsToLocation.put("third step", "path/step_definitions.java:11"); + long stepDuration = 1; + + String formatterOutput = runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, stepDuration); + + assertJsFunctionCallSequence(asList("" + + "formatter.uri(\"path/test.feature\");\n", "" + + "formatter.feature({\n" + + " \"description\": \"\",\n" + + " \"name\": \"feature name\",\n" + + " \"keyword\": \"Feature\"\n" + + "});\n", "" + + "formatter.scenarioOutline({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario Outline\",\n" + + " \"name\": \"outline name\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Then \",\n" + + " \"name\": \"\\u003carg\\u003e step\"\n" + + "});\n", "" + + "formatter.examples({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Examples\",\n" + + " \"name\": \"examples name\",\n" + + " \"rows\": [\n" + + " {\n" + + " \"cells\": [\n" + + " \"arg\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"cells\": [\n" + + " \"second\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"cells\": [\n" + + " \"third\"\n" + + " ]\n" + + " }\n" + + " ]\n" + + "});\n", "" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario Outline\",\n" + + " \"name\": \"outline name\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Then \",\n" + + " \"name\": \"second step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:7\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario Outline\",\n" + + " \"name\": \"outline name\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Then \",\n" + + " \"name\": \"third step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:11\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});"), + formatterOutput); + } + + @Test + public void should_handle_before_hooks() throws Throwable { + CucumberFeature feature = feature("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", result("passed")); + Map stepsToLocation = new HashMap(); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + List> hooks = new ArrayList>(); + hooks.add(TestHelper.hookEntry("before", result("passed"))); + long stepDuration = 1; + + String formatterOutput = runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, hooks, stepDuration); + + assertJsFunctionCallSequence(asList("" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario\",\n" + + " \"name\": \"scenario name\"\n" + + "});\n", "" + + "formatter.before({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n"), + formatterOutput); + } + + @Test + public void should_handle_after_hooks() throws Throwable { + CucumberFeature feature = feature("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", result("passed")); + Map stepsToLocation = new HashMap(); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + List> hooks = new ArrayList>(); + hooks.add(TestHelper.hookEntry("after", result("passed"))); + long stepDuration = 1; + + String formatterOutput = runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, hooks, stepDuration); + + assertJsFunctionCallSequence(asList("" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario\",\n" + + " \"name\": \"scenario name\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.after({\n" + + " \"status\": \"passed\"\n" + + "});\n"), + formatterOutput); + } + + @Test + public void should_handle_output_from_before_hooks() throws Throwable { + CucumberFeature feature = feature("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", result("passed")); + Map stepsToLocation = new HashMap(); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + List> hooks = new ArrayList>(); + hooks.add(TestHelper.hookEntry("before", result("passed"))); + List> hookActions = new ArrayList>(); + hookActions.add(createWriteHookAction("printed from hook")); + long stepDuration = 1; + + String formatterOutput = runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, hooks, hookActions, stepDuration); + + assertJsFunctionCallSequence(asList("" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario\",\n" + + " \"name\": \"scenario name\"\n" + + "});\n", "" + + "formatter.write(\"printed from hook\");\n", "" + + "formatter.before({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n"), + formatterOutput); + } + + @Test + public void should_handle_output_from_after_hooks() throws Throwable { + CucumberFeature feature = feature("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", result("passed")); + Map stepsToLocation = new HashMap(); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + List> hooks = new ArrayList>(); + hooks.add(TestHelper.hookEntry("after", result("passed"))); + List> hookActions = new ArrayList>(); + hookActions.add(createWriteHookAction("printed from hook")); + long stepDuration = 1; + + String formatterOutput = runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, hooks, hookActions, stepDuration); + + assertJsFunctionCallSequence(asList("" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario\",\n" + + " \"name\": \"scenario name\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.write(\"printed from hook\");\n", "" + + "formatter.after({\n" + + " \"status\": \"passed\"\n" + + "});\n"), + formatterOutput); + } + + @Test + public void should_handle_text_embeddings_from_before_hooks() throws Throwable { + CucumberFeature feature = feature("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", result("passed")); + Map stepsToLocation = new HashMap(); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + List> hooks = new ArrayList>(); + hooks.add(TestHelper.hookEntry("before", result("passed"))); + List> hookActions = new ArrayList>(); + hookActions.add(createEmbedHookAction("embedded from hook".getBytes("US-ASCII"), "text/ascii")); + long stepDuration = 1; + + String formatterOutput = runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, hooks, hookActions, stepDuration); + + assertJsFunctionCallSequence(asList("" + + "formatter.scenario({\n" + + " \"description\": \"\",\n" + + " \"keyword\": \"Scenario\",\n" + + " \"name\": \"scenario name\"\n" + + "});\n", "" + + "formatter.embedding(\"text/ascii\", \"embedded from hook\");\n", "" + + "formatter.before({\n" + + " \"status\": \"passed\"\n" + + "});\n", "" + + "formatter.step({\n" + + " \"keyword\": \"Given \",\n" + + " \"name\": \"first step\"\n" + + "});\n", "" + + "formatter.match({\n" + + " \"location\": \"path/step_definitions.java:3\"\n" + + "});\n", "" + + "formatter.result({\n" + + " \"status\": \"passed\"\n" + + "});\n"), + formatterOutput); + } + + private void assertJsFunctionCallSequence(List expectedList, String actual) { + Iterator expectedIterator = expectedList.iterator(); + String expected = expectedIterator.next(); + Matcher expectedMatcher = jsFunctionCallRegex.matcher(expected); + Matcher actualMatcher = jsFunctionCallRegex.matcher(actual); + assertTrue(jsFunctionCallMatchFailure(expected), expectedMatcher.find()); + boolean found = false; + while (actualMatcher.find()) { + if (matchFound(expectedMatcher, actualMatcher)) { + found = true; + break; + } + } + assertTrue(jsFunctionCallNotFoundMessage(actual, expected), found); + while (expectedIterator.hasNext()) { + expected = expectedIterator.next(); + expectedMatcher = jsFunctionCallRegex.matcher(expected); + assertTrue(jsFunctionCallMatchFailure(expected), expectedMatcher.find()); + assertTrue(jsFunctionCallNotFoundMessage(actual, expected), actualMatcher.find()); + if (!matchFound(expectedMatcher, actualMatcher)) { + fail(jsFunctionCallNotFoundMessage(actual, expected)); + } + } + } + + private String jsFunctionCallMatchFailure(String expected) { + return "The expected string: " + expected + ", does not match " + jsFunctionCallRegexString; + } + + private String jsFunctionCallNotFoundMessage(String actual, String expected) { + return "The expected js function call: " + expected + ", is not found in " + actual; + } + + private boolean matchFound(Matcher expectedMatcher, Matcher actualMatcher) { + String expectedFunction = expectedMatcher.group(1); + String actualFunction = actualMatcher.group(1); + if (!expectedFunction.equals(actualFunction)) { + return false; + } + String expectedArgument = expectedMatcher.group(2); + String actualArgumant = actualMatcher.group(2); + if (matchUsingJson(expectedArgument, actualArgumant)) { + JsonParser parser = new JsonParser(); + return parser.parse(expectedArgument).equals(parser.parse(actualArgumant)); + } else { + return expectedArgument.equals(actualArgumant); + } + } + + private boolean matchUsingJson(String expected, String actual) { + return expected.startsWith("{") && actual.startsWith("{"); + } + private void assertContains(String substring, String string) { if (string.indexOf(substring) == -1) { fail(String.format("[%s] not contained in [%s]", substring, string)); } } - private void runFeaturesWithFormatter(URL outputDir) throws IOException { + private void runFeaturesWithFormatter(URL outputDir) throws Throwable { final HTMLFormatter f = new HTMLFormatter(outputDir); -// f.uri("some\\windows\\path\\some.feature"); - //f.scenario(new Scenario(Collections.emptyList(), Collections.emptyList(), "Scenario", "some cukes", "", 10, "id")); -// f.embedding("image/png", "fakedata".getBytes("US-ASCII")); -// f.embedding("text/plain", "dodgy stack trace here".getBytes("US-ASCII")); -// f.done(); - f.close(); + CucumberFeature feature = feature("some\\windows\\path\\some.feature", "" + + "Feature:\n" + + " Scenario: some cukes\n" + + " Given first step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", result("passed")); + Map stepsToLocation = new HashMap(); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + List> hooks = new ArrayList>(); + hooks.add(TestHelper.hookEntry("after", result("passed"))); + hooks.add(TestHelper.hookEntry("after", result("passed"))); + List> hookActions = new ArrayList>(); + hookActions.add(createEmbedHookAction("fakedata".getBytes("US-ASCII"), "image/png")); + hookActions.add(createEmbedHookAction("dodgy stack trace here".getBytes("US-ASCII"), "text/plain")); + long stepHookDuration = 1; + + TestHelper.runFeatureWithFormatter(feature, stepsToResult, stepsToLocation, hooks, Collections.emptyList(), hookActions, stepHookDuration, f); + } + + private String runFeatureWithHTMLFormatter(final CucumberFeature feature, final Map stepsToResult, final Map stepsToLocation, final long stepHookDuration) throws Throwable { + return runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, Collections.>emptyList(), stepHookDuration); + } + + private String runFeatureWithHTMLFormatter(final CucumberFeature feature, final Map stepsToResult, final Map stepsToLocation, final List> hooks, final long stepHookDuration) throws Throwable { + return runFeatureWithHTMLFormatter(feature, stepsToResult, stepsToLocation, hooks, Collections.>emptyList(), stepHookDuration); + } + + private String runFeatureWithHTMLFormatter(final CucumberFeature feature, final Map stepsToResult, final Map stepsToLocation, final List> hooks, final List> hookActions, final long stepHookDuration) throws Throwable { + final StringBuilder out = new StringBuilder(); + final HTMLFormatter htmlFormatter = new HTMLFormatter(null, new NiceAppendable(out)); + TestHelper.runFeatureWithFormatter(feature, stepsToResult, stepsToLocation, hooks, Collections.emptyList(), hookActions, stepHookDuration, htmlFormatter); + return out.toString(); } }