From 72731d456af4ed173c12a3a2403706d45ad5b3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sun, 19 Jul 2015 16:47:50 +0200 Subject: [PATCH 01/13] Introduction of hierarchical tags - tags can now be tagged with other tags, making it possible to create tag hierarchies. These hierarchies are shown in the HTML5 report - the JSON report now represents tags more efficiently avoiding duplication of data. - the HTML5 report generator now generates a separate tags.js file that contains all tag definitions - beside supporting hierarchical tags now, the HTML5 App is largely refactored with this commit. Splitting its functionality into multiple files for better maintainability. This should not be visible to the end user though. --- .../com/tngtech/jgiven/annotation/IsTag.java | 13 +- .../config/AbstractJGivenConfiguraton.java | 2 +- .../jgiven/config/TagConfiguration.java | 68 +- .../com/tngtech/jgiven/impl/ScenarioBase.java | 2 +- .../html/DataTableScenarioHtmlWriter.java | 11 +- .../html/MultiCaseScenarioHtmlWriter.java | 6 +- .../report/html/ReportModelHtmlWriter.java | 21 +- .../report/html/ScenarioHtmlWriter.java | 254 ++-- .../html/StaticHtmlReportGenerator.java | 26 +- .../report/model/CompleteReportModel.java | 9 +- .../jgiven/report/model/ReportModel.java | 96 +- .../report/model/ReportModelBuilder.java | 75 +- .../jgiven/report/model/ScenarioModel.java | 22 +- .../com/tngtech/jgiven/report/model/Tag.java | 76 +- .../report/model/ReportModelBuilderTest.java | 69 + .../coffeemachine/ServeCoffeeTest.java | 2 +- .../tags/AnotherExampleSubCategory.java | 15 + .../examples/tags/CategoryWithValue.java | 15 + .../jgiven/examples/tags/ExampleCategory.java | 13 + .../examples/tags/ExampleSubCategory.java | 15 + .../examples/{annotation => tags}/Order.java | 2 +- .../tags/TagHierarchyExampleTest.java | 46 + jgiven-html5-report/README.md | 8 - jgiven-html5-report/package.json | 2 +- jgiven-html5-report/src/app/app.js | 1118 ----------------- .../src/app/css/jgivenreport.css | 83 +- jgiven-html5-report/src/app/index.html | 681 +++++----- jgiven-html5-report/src/app/lib/app.js | 22 + jgiven-html5-report/src/app/lib/chartCtrl.js | 25 + .../src/app/lib/classService.js | 142 +++ .../src/app/lib/dataService.js | 87 ++ .../src/app/lib/navigationCtrl.js | 20 + .../src/app/lib/optionService.js | 358 ++++++ jgiven-html5-report/src/app/lib/reportCtrl.js | 518 ++++++++ .../src/app/lib/searchService.js | 81 ++ jgiven-html5-report/src/app/lib/tagService.js | 222 ++++ jgiven-html5-report/src/app/lib/util.js | 85 ++ .../report/html5/Html5ReportGenerator.java | 23 +- .../tngtech/jgiven/report/html5/TagFile.java | 33 + .../jgiven/junit/ScenarioExecutionTest.java | 11 +- .../jgiven/junit/StepsAreReportedTest.java | 49 +- jgiven-tests/build.gradle | 1 + .../jgiven/tags/FeatureHtml5Report.java | 1 + .../jgiven/tags/FeatureHtmlReport.java | 1 + .../tngtech/jgiven/tags/FeatureReport.java | 12 + .../jgiven/tags/FeatureTextReport.java | 1 + .../jgiven/report/ThenReportGenerator.java | 24 +- .../jgiven/report/WhenReportGenerator.java | 2 +- ...ml5ReportStage.java => Html5AppStage.java} | 2 +- ...l5GeneratorTest.java => Html5AppTest.java} | 4 +- .../html5/Html5ReportGeneratorTest.java | 33 + ...ThenHtml5Report.java => ThenHtml5App.java} | 2 +- .../html5/ThenHtml5ReportGenerator.java | 5 + ...WhenHtml5Report.java => WhenHtml5App.java} | 2 +- .../html5/WhenHtml5ReportGenerator.java | 28 + .../jgiven/report/model/GivenReportModel.java | 1 + .../jgiven/report/model/ThenReportModel.java | 4 +- 57 files changed, 2796 insertions(+), 1753 deletions(-) create mode 100644 jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/AnotherExampleSubCategory.java create mode 100644 jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/CategoryWithValue.java create mode 100644 jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleCategory.java create mode 100644 jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleSubCategory.java rename jgiven-examples/src/test/java/com/tngtech/jgiven/examples/{annotation => tags}/Order.java (86%) create mode 100644 jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/TagHierarchyExampleTest.java delete mode 100644 jgiven-html5-report/src/app/app.js create mode 100644 jgiven-html5-report/src/app/lib/app.js create mode 100644 jgiven-html5-report/src/app/lib/chartCtrl.js create mode 100644 jgiven-html5-report/src/app/lib/classService.js create mode 100644 jgiven-html5-report/src/app/lib/dataService.js create mode 100644 jgiven-html5-report/src/app/lib/navigationCtrl.js create mode 100644 jgiven-html5-report/src/app/lib/optionService.js create mode 100644 jgiven-html5-report/src/app/lib/reportCtrl.js create mode 100644 jgiven-html5-report/src/app/lib/searchService.js create mode 100644 jgiven-html5-report/src/app/lib/tagService.js create mode 100644 jgiven-html5-report/src/app/lib/util.js create mode 100644 jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/TagFile.java create mode 100644 jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java rename jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/{Html5ReportStage.java => Html5AppStage.java} (90%) rename jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/{Html5GeneratorTest.java => Html5AppTest.java} (94%) create mode 100644 jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5ReportGeneratorTest.java rename jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/{ThenHtml5Report.java => ThenHtml5App.java} (93%) create mode 100644 jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5ReportGenerator.java rename jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/{WhenHtml5Report.java => WhenHtml5App.java} (96%) create mode 100644 jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5ReportGenerator.java diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/IsTag.java b/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/IsTag.java index 8c62474a4e..9c06eab4de 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/IsTag.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/IsTag.java @@ -71,10 +71,21 @@ Class descriptionGenerator() default DefaultTagDescriptionGenerator.class; /** - * An optional type description that overrides the default which is the name of the annotation. + * @deprecated use {@link #name()} instead */ + @Deprecated String type() default ""; + /** + * An optional name that overrides the default which is the name of the annotation. + *

+ * It is possible that multiple annotations have the same type name. However, in this case every + * annotation must have a specified value that must be unique. + *

+ * @since 0.7.4 + */ + String name() default ""; + /** * Whether the type should be prepended to the tag if the tag has a value. */ diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/config/AbstractJGivenConfiguraton.java b/jgiven-core/src/main/java/com/tngtech/jgiven/config/AbstractJGivenConfiguraton.java index 4622f69319..c61cca7324 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/config/AbstractJGivenConfiguraton.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/config/AbstractJGivenConfiguraton.java @@ -9,7 +9,7 @@ public abstract class AbstractJGivenConfiguraton { private final Map, TagConfiguration> tagConfigurations = Maps.newHashMap(); public final TagConfiguration.Builder configureTag( Class tagAnnotation ) { - TagConfiguration configuration = new TagConfiguration(); + TagConfiguration configuration = new TagConfiguration( tagAnnotation ); tagConfigurations.put( tagAnnotation, configuration ); return new TagConfiguration.Builder( configuration ); } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/config/TagConfiguration.java b/jgiven-core/src/main/java/com/tngtech/jgiven/config/TagConfiguration.java index ec4c58d18f..4845902eeb 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/config/TagConfiguration.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/config/TagConfiguration.java @@ -1,7 +1,10 @@ package com.tngtech.jgiven.config; +import java.lang.annotation.Annotation; +import java.util.List; + +import com.google.common.collect.Lists; import com.tngtech.jgiven.annotation.DefaultTagDescriptionGenerator; -import com.tngtech.jgiven.annotation.IsTag; import com.tngtech.jgiven.annotation.TagDescriptionGenerator; /** @@ -10,6 +13,7 @@ * @see com.tngtech.jgiven.annotation.IsTag for a documentation of the different values. */ public class TagConfiguration { + private final String annotationType; private boolean ignoreValue; private boolean explodeArray = true; private boolean prependType; @@ -18,7 +22,16 @@ public class TagConfiguration { private String color = ""; private String cssClass = ""; private Class descriptionGenerator = DefaultTagDescriptionGenerator.class; - private String type = ""; + private String name = ""; + private List tags = Lists.newArrayList(); + + public TagConfiguration( Class tagAnnotation ) { + this.annotationType = tagAnnotation.getSimpleName(); + } + + public static Builder builder( Class tagAnnotation ) { + return new Builder( new TagConfiguration( tagAnnotation ) ); + } public static class Builder { final TagConfiguration configuration; @@ -52,8 +65,17 @@ public Builder descriptionGenerator( Class de return this; } + /** + * @deprecated use {@link #name(String)} instead + */ + @Deprecated public Builder type( String s ) { - configuration.type = s; + configuration.name = s; + return this; + } + + public Builder name( String s ) { + configuration.name = s; return this; } @@ -72,6 +94,15 @@ public Builder color( String color ) { return this; } + public Builder tags( List tags ) { + configuration.tags = tags; + return this; + } + + public TagConfiguration build() { + return configuration; + } + } /** @@ -100,10 +131,20 @@ public Class getDescriptionGenerator() { /** * {@link com.tngtech.jgiven.annotation.IsTag#type()} + * @deprecated use {@link #getName()} instead * @see com.tngtech.jgiven.annotation.IsTag */ + @Deprecated public String getType() { - return type; + return name; + } + + /** + * {@link com.tngtech.jgiven.annotation.IsTag#name()} + * @see com.tngtech.jgiven.annotation.IsTag + */ + public String getName() { + return name; } /** @@ -146,17 +187,12 @@ public String getCssClass() { return cssClass; } - public static TagConfiguration fromIsTag( IsTag isTag ) { - TagConfiguration result = new TagConfiguration(); - result.defaultValue = isTag.value(); - result.description = isTag.description(); - result.explodeArray = isTag.explodeArray(); - result.ignoreValue = isTag.ignoreValue(); - result.prependType = isTag.prependType(); - result.type = isTag.type(); - result.descriptionGenerator = isTag.descriptionGenerator(); - result.cssClass = isTag.cssClass(); - result.color = isTag.color(); - return result; + public List getTags() { + return tags; } + + public String getAnnotationType() { + return annotationType; + } + } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java index d4f6cced24..b7aaced38f 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java @@ -17,7 +17,7 @@ public void setModel( ReportModel scenarioCollectionModel ) { } public ReportModel getModel() { - return modelBuilder.getScenarioCollectionModel(); + return modelBuilder.getReportModel(); } public T addStage( Class stepsClass ) { diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/DataTableScenarioHtmlWriter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/DataTableScenarioHtmlWriter.java index 10ef5dca11..bcd43d113b 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/DataTableScenarioHtmlWriter.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/DataTableScenarioHtmlWriter.java @@ -2,15 +2,12 @@ import java.io.PrintWriter; -import com.tngtech.jgiven.report.model.ScenarioCaseModel; -import com.tngtech.jgiven.report.model.ScenarioModel; -import com.tngtech.jgiven.report.model.StepModel; -import com.tngtech.jgiven.report.model.Word; +import com.tngtech.jgiven.report.model.*; public class DataTableScenarioHtmlWriter extends ScenarioHtmlWriter { - public DataTableScenarioHtmlWriter( PrintWriter writer ) { - super( writer ); + public DataTableScenarioHtmlWriter( PrintWriter writer, ReportModel reportModel ) { + super( writer, reportModel ); } @Override @@ -74,7 +71,7 @@ public void visit( StepModel stepModel ) { } @Override - String formatValue(Word value) { + String formatValue( Word value ) { String paramName = findParameterName( value ); return "<" + paramName + ">"; } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/MultiCaseScenarioHtmlWriter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/MultiCaseScenarioHtmlWriter.java index ce32011849..e691058982 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/MultiCaseScenarioHtmlWriter.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/MultiCaseScenarioHtmlWriter.java @@ -2,10 +2,12 @@ import java.io.PrintWriter; +import com.tngtech.jgiven.report.model.ReportModel; + public class MultiCaseScenarioHtmlWriter extends ScenarioHtmlWriter { - public MultiCaseScenarioHtmlWriter( PrintWriter writer ) { - super( writer ); + public MultiCaseScenarioHtmlWriter( PrintWriter writer, ReportModel reportModel ) { + super( writer, reportModel ); } } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ReportModelHtmlWriter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ReportModelHtmlWriter.java index 28b1d5c589..c242a51cfb 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ReportModelHtmlWriter.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ReportModelHtmlWriter.java @@ -2,12 +2,7 @@ import static java.lang.String.format; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.text.DateFormat; import java.util.Date; @@ -19,16 +14,13 @@ import com.tngtech.jgiven.impl.util.PrintWriterUtil; import com.tngtech.jgiven.impl.util.ResourceUtil; import com.tngtech.jgiven.impl.util.Version; -import com.tngtech.jgiven.report.model.ReportModel; -import com.tngtech.jgiven.report.model.ReportModelVisitor; -import com.tngtech.jgiven.report.model.ReportStatistics; -import com.tngtech.jgiven.report.model.ScenarioModel; -import com.tngtech.jgiven.report.model.StatisticsCalculator; +import com.tngtech.jgiven.report.model.*; public class ReportModelHtmlWriter extends ReportModelVisitor { protected final PrintWriter writer; protected final HtmlWriterUtils utils; private ReportStatistics statistics; + private ReportModel reportModel; public ReportModelHtmlWriter( PrintWriter writer ) { this.writer = writer; @@ -61,7 +53,7 @@ private void closeDiv() { } public void write( ScenarioModel model ) { - writeHtmlHeader(model.getClassName()); + writeHtmlHeader( model.getClassName() ); model.accept( this ); writeHtmlFooter(); } @@ -142,6 +134,7 @@ public static void writeToFile( File file, ReportModel model, HtmlTocWriter html @Override public void visit( ReportModel reportModel ) { + this.reportModel = reportModel; writer.println( "
" ); writeHeader( reportModel ); writer.println( "
" ); @@ -191,9 +184,9 @@ public void visitEnd( ReportModel reportModel ) { public void visit( ScenarioModel scenarioModel ) { ScenarioHtmlWriter scenarioHtmlWriter; if( scenarioModel.isCasesAsTable() ) { - scenarioHtmlWriter = new DataTableScenarioHtmlWriter( writer ); + scenarioHtmlWriter = new DataTableScenarioHtmlWriter( writer, reportModel ); } else { - scenarioHtmlWriter = new MultiCaseScenarioHtmlWriter( writer ); + scenarioHtmlWriter = new MultiCaseScenarioHtmlWriter( writer, reportModel ); } scenarioModel.accept( scenarioHtmlWriter ); } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ScenarioHtmlWriter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ScenarioHtmlWriter.java index 7f3fbf371a..6f275fd53e 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ScenarioHtmlWriter.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ScenarioHtmlWriter.java @@ -14,101 +14,103 @@ public class ScenarioHtmlWriter extends ReportModelVisitor { final PrintWriter writer; + final ReportModel reportModel; ScenarioModel scenarioModel; ScenarioCaseModel scenarioCase; HtmlWriterUtils utils; - public ScenarioHtmlWriter( PrintWriter writer ) { + public ScenarioHtmlWriter(PrintWriter writer, ReportModel reportModel) { this.writer = writer; - this.utils = new HtmlWriterUtils( writer ); + this.reportModel = reportModel; + this.utils = new HtmlWriterUtils(writer); } @Override - public void visit( ScenarioModel scenarioModel ) { + public void visit(ScenarioModel scenarioModel) { this.scenarioModel = scenarioModel; - writer.println( "
" ); + writer.println("
"); String id = scenarioModel.getClassName() + ":" + scenarioModel.getDescription(); - writer.print( format( "

", id ) ); + writer.print(format("

", id)); - writeStatusIcon( scenarioModel.getExecutionStatus() ); + writeStatusIcon(scenarioModel.getExecutionStatus()); - writer.print( "" ); - writer.print( " " + WordUtil.capitalize( scenarioModel.getDescription() ) ); - writer.print( "" ); + writer.print(""); + writer.print(" " + WordUtil.capitalize(scenarioModel.getDescription())); + writer.print(""); int numberOfCases = scenarioModel.getScenarioCases().size(); - if( numberOfCases > 1 ) { - writer.print( "" + numberOfCases + "" ); + if (numberOfCases > 1) { + writer.print("" + numberOfCases + ""); } - utils.writeDuration( scenarioModel.getDurationInNanos() ); + utils.writeDuration(scenarioModel.getDurationInNanos()); - writer.println( "

" ); + writer.println(""); - writeTagLine( scenarioModel ); - writer.println( "" ); + writer.println(format("", + scenarioModel.getClassName(), scenarioModel.getClassName())); + writer.println("
"); + writer.println("
"); } @Override - public void visit( ScenarioCaseModel scenarioCase ) { + public void visit(ScenarioCaseModel scenarioCase) { this.scenarioCase = scenarioCase; - printCaseHeader( scenarioCase ); + printCaseHeader(scenarioCase); - if( hasMultipleExplicitCases() ) { - writer.println( ""); } - writer.println( "
" ); + writer.println("
"); } @Override - public void visit( StepModel stepModel ) { - writer.print( "
  • " ); + public void visit(StepModel stepModel) { + writer.print("
  • "); boolean firstWord = true; - for( Word word : stepModel.words ) { - if( !firstWord ) { - writer.print( ' ' ); + for (Word word : stepModel.words) { + if (!firstWord) { + writer.print(' '); } - if( word.isDataTable() ) { - writeDataTable( word ); + if (word.isDataTable()) { + writeDataTable(word); } else { - String text = HtmlEscapers.htmlEscaper().escape( word.getValue() ); - String diffClass = diffClass( word ); - if( firstWord && !word.isIntroWord() ) { - writer.print( "" ); + String text = HtmlEscapers.htmlEscaper().escape(word.getValue()); + String diffClass = diffClass(word); + if (firstWord && !word.isIntroWord()) { + writer.print(""); } - if( firstWord && word.isIntroWord() ) { - writer.print( format( "%s", WordUtil.capitalize( text ) ) ); - } else if( word.isArg() ) { - printArg( word ); + if (firstWord && word.isIntroWord()) { + writer.print(format("%s", WordUtil.capitalize(text))); + } else if (word.isArg()) { + printArg(word); } else { - if( word.isDifferent() ) { - writer.print( format( " %s", diffClass, text ) ); + if (word.isDifferent()) { + writer.print(format(" %s", diffClass, text)); } else { - writer.print( " " + text + "" ); + writer.print(" " + text + ""); } } } @@ -190,84 +192,84 @@ public void visit( StepModel stepModel ) { } StepStatus status = stepModel.getStatus(); - if( status != StepStatus.PASSED ) { + if (status != StepStatus.PASSED) { String lowerCase = status.toString().toLowerCase(); - writer.print( format( " %s", WordUtil.camelCase( lowerCase ), lowerCase.replace( '_', ' ' ) ) ); + writer.print(format(" %s", WordUtil.camelCase(lowerCase), lowerCase.replace('_', ' '))); } - if( stepModel.hasExtendedDescription() ) { - String extendedId = "extDesc" + System.identityHashCode( stepModel ); - if( stepModel.hasExtendedDescription() ) { - writer.print( " i" ); + if (stepModel.hasExtendedDescription()) { + String extendedId = "extDesc" + System.identityHashCode(stepModel); + if (stepModel.hasExtendedDescription()) { + writer.print(" i"); } - utils.writeDuration( stepModel.getDurationInNanos() ); - writeExtendedDescription( stepModel, extendedId ); + utils.writeDuration(stepModel.getDurationInNanos()); + writeExtendedDescription(stepModel, extendedId); } else { - utils.writeDuration( stepModel.getDurationInNanos() ); + utils.writeDuration(stepModel.getDurationInNanos()); } - writer.println( "
  • " ); + writer.println(""); } - private void writeDataTable( Word word ) { - writer.println( "" ); + private void writeDataTable(Word word) { + writer.println("
    "); boolean firstRow = true; DataTable dataTable = word.getArgumentInfo().getDataTable(); HeaderType headerType = dataTable.getHeaderType(); - for( List row : dataTable.getData() ) { - writer.println( "" ); + for (List row : dataTable.getData()) { + writer.println(""); boolean firstColumn = true; - for( String value : row ) { + for (String value : row) { boolean th = firstRow && headerType.isHorizontal() || firstColumn && headerType.isVertical(); - writer.println( th ? "" ); + writer.println(th ? "" : ""); firstColumn = false; } - writer.println( "" ); + writer.println(""); firstRow = false; } - writer.println( "
    " : "" ); + writer.println(th ? "" : ""); - String escapedValue = escapeToHtml( value ); - String multiLine = value.contains( "
    " ) ? " multiline" : ""; - writer.print( format( "%s", multiLine, escapedValue ) ); + String escapedValue = escapeToHtml(value); + String multiLine = value.contains("
    ") ? " multiline" : ""; + writer.print(format("%s", multiLine, escapedValue)); - writer.println( th ? "" : "
    " ); + writer.println(""); } - private void writeExtendedDescription( StepModel stepModel, String id ) { - writer.write( "" ); + private void writeExtendedDescription(StepModel stepModel, String id) { + writer.write(""); } - private String diffClass( Word word ) { + private String diffClass(Word word) { return word.isDifferent() ? " diff" : ""; } - private void printArg( Word word ) { - String value = word.getArgumentInfo().isParameter() ? formatValue( word ) : HtmlEscapers.htmlEscaper().escape( - word.getFormattedValue() ); - printArgValue( word, value ); + private void printArg(Word word) { + String value = word.getArgumentInfo().isParameter() ? formatValue(word) : HtmlEscapers.htmlEscaper().escape( + word.getFormattedValue()); + printArgValue(word, value); } - private void printArgValue( Word word, String value ) { - value = escapeToHtml( value ); - String multiLine = value.contains( "
    " ) ? " multiline" : ""; + private void printArgValue(Word word, String value) { + value = escapeToHtml(value); + String multiLine = value.contains("
    ") ? " multiline" : ""; String caseClass = word.getArgumentInfo().isParameter() ? "caseArgument" : "argument"; - writer.print( format( "%s", caseClass, multiLine, diffClass( word ), value ) ); + writer.print(format("%s", caseClass, multiLine, diffClass(word), value)); } - private String escapeToHtml( String value ) { - return value.replaceAll( "(\r\n|\n)", "
    " ); + private String escapeToHtml(String value) { + return value.replaceAll("(\r\n|\n)", "
    "); } - String formatValue( Word word ) { - return HtmlEscapers.htmlEscaper().escape( word.getValue() ); + String formatValue(Word word) { + return HtmlEscapers.htmlEscaper().escape(word.getValue()); } } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/StaticHtmlReportGenerator.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/StaticHtmlReportGenerator.java index e04f77824e..badd1ed425 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/StaticHtmlReportGenerator.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/StaticHtmlReportGenerator.java @@ -69,11 +69,12 @@ public void writeEnd() { } - private void writeScenarios( HtmlTocWriter tocWriter, List failedScenarios, String name, String fileName ) { - ReportModel completeReportModel = new ReportModel(); - completeReportModel.setScenarios( failedScenarios ); - completeReportModel.setClassName( name ); - ReportModelHtmlWriter.writeModelToFile( completeReportModel, tocWriter, new File( targetDirectory, fileName ) ); + private void writeScenarios( HtmlTocWriter tocWriter, List scenarios, String name, String fileName ) { + ReportModel reportModel = new ReportModel(); + reportModel.setScenarios( scenarios ); + reportModel.setClassName( name ); + reportModel.setTagMap( this.completeReportModel.getTagIdMap() ); + ReportModelHtmlWriter.writeModelToFile( reportModel, tocWriter, new File( targetDirectory, fileName ) ); } private void writeTagFiles( HtmlTocWriter tocWriter ) { @@ -84,20 +85,21 @@ private void writeTagFiles( HtmlTocWriter tocWriter ) { private void writeTagFile( Tag tag, List value, HtmlTocWriter tocWriter ) { try { - ReportModel completeReportModel = new ReportModel(); - completeReportModel.setClassName( tag.getName() ); + ReportModel reportModel = new ReportModel(); + reportModel.setClassName( tag.getName() ); if( tag.getValues().isEmpty() ) { - completeReportModel.setClassName( completeReportModel.getClassName() + "." + tag.getValueString() ); + reportModel.setClassName( reportModel.getClassName() + "." + tag.getValueString() ); } - completeReportModel.setScenarios( value ); - completeReportModel.setDescription( tag.getDescription() ); + reportModel.setScenarios( value ); + reportModel.setDescription( tag.getDescription() ); + reportModel.setTagMap( completeReportModel.getTagIdMap() ); String fileName = HtmlTocWriter.tagToFilename( tag ); File targetFile = new File( targetDirectory, fileName ); - ReportModelHtmlWriter.writeToFile( targetFile, completeReportModel, tocWriter ); + ReportModelHtmlWriter.writeToFile( targetFile, reportModel, tocWriter ); } catch( Exception e ) { - log.error( "Error while trying to write HTML file for tag " + tag.getName() ); + log.error( "Error while trying to write HTML file for tag " + tag.getName(), e ); } } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/CompleteReportModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/CompleteReportModel.java index d4130eb1f8..89a4b76b32 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/CompleteReportModel.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/CompleteReportModel.java @@ -19,16 +19,19 @@ public class CompleteReportModel { protected final List failedScenarios = Lists.newArrayList(); protected final List pendingScenarios = Lists.newArrayList(); protected final List allScenarios = Lists.newArrayList(); + protected final Map tagIdMap = Maps.newLinkedHashMap(); public void addModelFile( ReportModelFile modelFile ) { ReportModel model = modelFile.model; for( ScenarioModel scenario : model.getScenarios() ) { - for( Tag tag : scenario.getTags() ) { + for( String tagId : scenario.getTagIds() ) { + Tag tag = model.getTagWithId( tagId ); addToMap( tag, scenario ); } } + tagIdMap.putAll( model.getTagMap() ); ReportStatistics statistics = new StatisticsCalculator().getStatistics( model ); statisticsMap.put( modelFile, statistics ); @@ -82,4 +85,8 @@ public List getScenariosByTag( Tag tag ) { public List getAllReportModels() { return models; } + + public Map getTagIdMap() { + return tagIdMap; + } } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModel.java index e767baad38..63a7e96f7c 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModel.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModel.java @@ -1,14 +1,13 @@ package com.tngtech.jgiven.report.model; -import java.util.Collections; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.List; +import java.util.*; import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.tngtech.jgiven.impl.util.AssertionUtil; public class ReportModel { /** @@ -23,57 +22,59 @@ public class ReportModel { private List scenarios = Lists.newArrayList(); - public void accept( ReportModelVisitor visitor ) { - visitor.visit( this ); + private Map tagMap = Maps.newLinkedHashMap(); + + public void accept(ReportModelVisitor visitor) { + visitor.visit(this); List sorted = sortByDescription(); - for( ScenarioModel m : sorted ) { - m.accept( visitor ); + for (ScenarioModel m : sorted) { + m.accept(visitor); } - visitor.visitEnd( this ); + visitor.visitEnd(this); } private List sortByDescription() { - List sorted = Lists.newArrayList( getScenarios() ); - Collections.sort( sorted, new Comparator() { + List sorted = Lists.newArrayList(getScenarios()); + Collections.sort(sorted, new Comparator() { @Override - public int compare( ScenarioModel o1, ScenarioModel o2 ) { - return o1.getDescription().toLowerCase().compareTo( o2.getDescription().toLowerCase() ); + public int compare(ScenarioModel o1, ScenarioModel o2) { + return o1.getDescription().toLowerCase().compareTo(o2.getDescription().toLowerCase()); } - } ); + }); return sorted; } public ScenarioModel getLastScenarioModel() { - return getScenarios().get( getScenarios().size() - 1 ); + return getScenarios().get(getScenarios().size() - 1); } - public Optional findScenarioModel( String scenarioDescription ) { - for( ScenarioModel model : getScenarios() ) { - if( model.getDescription().equals( scenarioDescription ) ) { - return Optional.of( model ); + public Optional findScenarioModel(String scenarioDescription) { + for (ScenarioModel model : getScenarios()) { + if (model.getDescription().equals(scenarioDescription)) { + return Optional.of(model); } } return Optional.absent(); } public StepModel getFirstStepModelOfLastScenario() { - return getLastScenarioModel().getCase( 0 ).getStep( 0 ); + return getLastScenarioModel().getCase(0).getStep(0); } - public void addScenarioModel( ScenarioModel currentScenarioModel ) { - getScenarios().add( currentScenarioModel ); + public void addScenarioModel(ScenarioModel currentScenarioModel) { + getScenarios().add(currentScenarioModel); } public String getSimpleClassName() { - return Iterables.getLast( Splitter.on( '.' ).split( getClassName() ) ); + return Iterables.getLast(Splitter.on('.').split(getClassName())); } public String getDescription() { return description; } - public void setDescription( String description ) { + public void setDescription(String description) { this.description = description; } @@ -81,7 +82,7 @@ public String getClassName() { return className; } - public void setClassName( String className ) { + public void setClassName(String className) { this.className = className; } @@ -89,36 +90,59 @@ public List getScenarios() { return scenarios; } - public void setScenarios( List scenarios ) { + public void setScenarios(List scenarios) { this.scenarios = scenarios; } public String getPackageName() { - int index = this.className.lastIndexOf( '.' ); - if( index == -1 ) { + int index = this.className.lastIndexOf('.'); + if (index == -1) { return ""; } - return this.className.substring( 0, index ); + return this.className.substring(0, index); } public List getFailedScenarios() { - return getScenariosWithStatus( ExecutionStatus.FAILED ); + return getScenariosWithStatus(ExecutionStatus.FAILED); } public List getPendingScenarios() { - return getScenariosWithStatus( ExecutionStatus.NONE_IMPLEMENTED, ExecutionStatus.PARTIALLY_IMPLEMENTED ); + return getScenariosWithStatus(ExecutionStatus.NONE_IMPLEMENTED, ExecutionStatus.PARTIALLY_IMPLEMENTED); } - public List getScenariosWithStatus( ExecutionStatus first, ExecutionStatus... rest ) { - EnumSet stati = EnumSet.of( first, rest ); + public List getScenariosWithStatus(ExecutionStatus first, ExecutionStatus... rest) { + EnumSet stati = EnumSet.of(first, rest); List result = Lists.newArrayList(); - for( ScenarioModel m : scenarios ) { + for (ScenarioModel m : scenarios) { ExecutionStatus executionStatus = m.getExecutionStatus(); - if( stati.contains( executionStatus ) ) { - result.add( m ); + if (stati.contains(executionStatus)) { + result.add(m); } } return result; } + public void addTag(Tag tag) { + this.tagMap.put(tag.toIdString(), tag); + } + + public void addTags(List tags) { + for (Tag tag : tags) { + addTag(tag); + } + } + + public Tag getTagWithId(String tagId) { + Tag tag = this.tagMap.get(tagId); + AssertionUtil.assertNotNull(tag, "Could not find tag with id " + tagId); + return tag; + } + + public Map getTagMap() { + return tagMap; + } + + public void setTagMap(Map tagMap) { + this.tagMap = tagMap; + } } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java index 5380c989bb..d311744ee5 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java @@ -255,7 +255,7 @@ private static String nameWithoutUnderlines( Method paramMethod ) { return paramMethod.getName().replace( '_', ' ' ); } - public ReportModel getScenarioCollectionModel() { + public ReportModel getReportModel() { return reportModel; } @@ -340,9 +340,11 @@ private void readAnnotations( Method method ) { } } - private void addTags( Annotation[] annotations ) { + public void addTags( Annotation... annotations ) { for( Annotation annotation : annotations ) { - this.currentScenarioModel.addTags( toTags( annotation ) ); + List tags = toTags( annotation ); + this.reportModel.addTags( tags ); + this.currentScenarioModel.addTags( tags ); } } @@ -351,7 +353,7 @@ public List toTags( Annotation annotation ) { IsTag isTag = annotationType.getAnnotation( IsTag.class ); TagConfiguration tagConfig; if( isTag != null ) { - tagConfig = TagConfiguration.fromIsTag( isTag ); + tagConfig = fromIsTag( isTag, annotation ); } else { tagConfig = configuration.getTagConfiguration( annotationType ); } @@ -360,14 +362,12 @@ public List toTags( Annotation annotation ) { return Collections.emptyList(); } - String type = annotationType.getSimpleName(); + Tag tag = new Tag( tagConfig.getAnnotationType() ); - if( !Strings.isNullOrEmpty( tagConfig.getType() ) ) { - type = tagConfig.getType(); + if( !Strings.isNullOrEmpty( tagConfig.getName() ) ) { + tag.setName( tagConfig.getName() ); } - Tag tag = new Tag( type ); - if( tagConfig.isPrependType() ) { tag.setPrependType( true ); } @@ -390,6 +390,8 @@ public List toTags( Annotation annotation ) { return Arrays.asList( tag ); } + tag.setTags( tagConfig.getTags() ); + try { Method method = annotationType.getMethod( "value" ); value = method.invoke( annotation ); @@ -397,7 +399,8 @@ public List toTags( Annotation annotation ) { if( value.getClass().isArray() ) { Object[] objectArray = (Object[]) value; if( tagConfig.isExplodeArray() ) { - return getExplodedTags( tag, objectArray, annotation, tagConfig ); + List explodedTags = getExplodedTags( tag, objectArray, annotation, tagConfig ); + return explodedTags; } tag.setValue( toStringList( objectArray ) ); @@ -415,6 +418,50 @@ public List toTags( Annotation annotation ) { return Arrays.asList( tag ); } + public TagConfiguration fromIsTag( IsTag isTag, Annotation annotation ) { + + String name = Strings.isNullOrEmpty( isTag.name() ) ? isTag.type() : isTag.name(); + + return TagConfiguration.builder( annotation.annotationType() ) + .defaultValue( isTag.value() ) + .description( isTag.description() ) + .explodeArray( isTag.explodeArray() ) + .ignoreValue( isTag.ignoreValue() ) + .prependType( isTag.prependType() ) + .name( name ) + .descriptionGenerator( isTag.descriptionGenerator() ) + .cssClass( isTag.cssClass() ) + .color( isTag.color() ) + .tags( getTagNames( isTag, annotation ) ) + .build(); + + } + + private List getTagNames( IsTag isTag, Annotation annotation ) { + List tags = getTags( isTag, annotation ); + reportModel.addTags( tags ); + List tagNames = Lists.newArrayList(); + for( Tag tag : tags ) { + tagNames.add( tag.toIdString() ); + } + return tagNames; + } + + private List getTags( IsTag isTag, Annotation annotation ) { + List allTags = Lists.newArrayList(); + + for( Annotation a : annotation.annotationType().getAnnotations() ) { + if( a.annotationType().isAnnotationPresent( IsTag.class ) ) { + List tags = toTags( a ); + for( Tag tag : tags ) { + allTags.add( tag ); + } + } + } + + return allTags; + } + private List toStringList( Object[] value ) { Object[] array = value; List values = Lists.newArrayList(); @@ -436,12 +483,8 @@ private String getDescriptionFromGenerator( TagConfiguration tagConfiguration, A private List getExplodedTags( Tag originalTag, Object[] values, Annotation annotation, TagConfiguration tagConfig ) { List result = Lists.newArrayList(); for( Object singleValue : values ) { - Tag newTag = new Tag( originalTag.getName(), String.valueOf( singleValue ) ); - newTag.setDescription( originalTag.getDescription() ); - newTag.setPrependType( originalTag.isPrependType() ); - newTag.setDescription( getDescriptionFromGenerator( tagConfig, annotation, singleValue ) ); - newTag.setColor( originalTag.getColor() ); - newTag.setCssClass( originalTag.getCssClass() ); + Tag newTag = originalTag.copy(); + newTag.setValue( String.valueOf( singleValue ) ); result.add( newTag ); } return result; diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioModel.java index 4d93e42758..29005ae76a 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioModel.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioModel.java @@ -12,7 +12,11 @@ public class ScenarioModel { private String className; private String testMethodName; private String description; - private Set tags = Sets.newLinkedHashSet(); + + /** + * A list of tag ids + */ + private Set tagIds = Sets.newLinkedHashSet(); private boolean notImplementedYet; private List explicitParameters = Lists.newArrayList(); private List derivedParameters = Lists.newArrayList(); @@ -30,7 +34,7 @@ public void accept( ReportModelVisitor visitor ) { } public void addCase( ScenarioCaseModel scenarioCase ) { - scenarioCase.setCaseNr(scenarioCases.size() + 1); + scenarioCase.setCaseNr( scenarioCases.size() + 1 ); scenarioCases.add( scenarioCase ); } @@ -48,11 +52,13 @@ public ScenarioCaseModel getCase( int i ) { } public void addTag( Tag tag ) { - tags.add( tag ); + tagIds.add(tag.toIdString()); } public void addTags( List tags ) { - this.tags.addAll( tags ); + for( Tag tag : tags ) { + addTag( tag ); + } } public void addParameterNames( String... params ) { @@ -72,8 +78,8 @@ public List getScenarioCases() { return scenarioCases; } - public List getTags() { - return Lists.newArrayList( tags ); + public List getTagIds() { + return Lists.newArrayList(tagIds); } public boolean isCasesAsTable() { @@ -132,8 +138,8 @@ public void setDescription( String description ) { this.description = description; } - public void setTags( Set tags ) { - this.tags = tags; + public void setTagIds(Set tagIds) { + this.tagIds = tagIds; } public boolean isNotImplementedYet() { diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/Tag.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/Tag.java index 2ee4b18a32..badac1b20e 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/Tag.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/Tag.java @@ -12,9 +12,14 @@ */ public class Tag { /** - * The name/type of this tag + * The type of the annotation of the tag */ - private final String name; + private final String type; + + /** + * An optional name of the tag. If not set, the type is the name + */ + private String name; /** * An optional value @@ -44,16 +49,31 @@ public class Tag { */ private String cssClass; - public Tag( String name ) { - this.name = name; + /** + * An optional (maybe null) list of tags that this tag is tagged with. + * The tags are normalized as follows: [-value]. + */ + private List tags; + + public Tag( String type ) { + this.type = type; } - public Tag( String name, Object value ) { - this.name = name; + public Tag( String type, Object value ) { + this( type ); this.value = value; } + public Tag( String type, String name, Object value ) { + this( type, value ); + this.name = name; + } + public String getName() { + if( name == null ) { + return type; + } + return name; } @@ -132,9 +152,16 @@ public String getValueString() { return Joiner.on( ", " ).join( getValues() ); } + public String toIdString() { + if( value != null ) { + return type + "-" + getValueString(); + } + return type; + } + @Override public int hashCode() { - return Objects.hashCode( getName(), value ); + return Objects.hashCode( getType(), getName(), value ); } @Override @@ -149,7 +176,8 @@ public boolean equals( Object obj ) { return false; } Tag other = (Tag) obj; - return Objects.equal( getName(), other.getName() ) + return Objects.equal( getType(), other.getType() ) + && Objects.equal( getName(), other.getName() ) && Objects.equal( value, other.value ); } @@ -169,4 +197,36 @@ public String toEscapedString() { static String escape( String string ) { return string.replaceAll( "[^\\p{Alnum}-]", "_" ); } + + public void setName( String name ) { + this.name = name; + } + + public String getType() { + return type; + } + + public List getTags() { + if( tags == null ) { + return Collections.emptyList(); + } + return tags; + } + + public void setTags( List tags ) { + if( tags != null && !tags.isEmpty() ) { + this.tags = tags; + } + } + + public Tag copy() { + Tag tag = new Tag( type, name, value ); + tag.cssClass = this.cssClass; + tag.color = this.color; + tag.description = this.description; + tag.prependType = this.prependType; + tag.tags = this.tags; + return tag; + } + } diff --git a/jgiven-core/src/test/java/com/tngtech/jgiven/report/model/ReportModelBuilderTest.java b/jgiven-core/src/test/java/com/tngtech/jgiven/report/model/ReportModelBuilderTest.java index 5990aa0caa..0696a7c154 100644 --- a/jgiven-core/src/test/java/com/tngtech/jgiven/report/model/ReportModelBuilderTest.java +++ b/jgiven-core/src/test/java/com/tngtech/jgiven/report/model/ReportModelBuilderTest.java @@ -25,6 +25,8 @@ @RunWith( DataProviderRunner.class ) public class ReportModelBuilderTest extends ScenarioTestBase { + private ReportModelBuilder reportModelBuilder; + @DataProvider public static Object[][] testData() { return new Object[][] { @@ -108,6 +110,44 @@ public void testAnnotationWithValueParsing() throws Exception { assertThat( tags.get( 0 ).getValues() ).containsExactly( "testvalue" ); } + @IsTag( name = "AnotherName" ) + @Retention( RetentionPolicy.RUNTIME ) + @interface AnnotationWithName {} + + @AnnotationWithName( ) + static class AnnotationWithNameTestClass {} + + @Test + public void testAnnotationWithName() throws Exception { + ReportModelBuilder modelBuilder = new ReportModelBuilder(); + List tags = modelBuilder.toTags( AnnotationWithNameTestClass.class.getAnnotations()[0] ); + assertThat( tags ).hasSize( 1 ); + Tag tag = tags.get( 0 ); + assertThat( tag.getName() ).isEqualTo( "AnotherName" ); + assertThat( tag.getValues() ).isEmpty(); + assertThat( tag.toIdString() ).isEqualTo( "AnnotationWithName" ); + } + + @IsTag( ignoreValue = true ) + @Retention( RetentionPolicy.RUNTIME ) + @interface AnnotationWithIgnoredValue { + String value(); + } + + @AnnotationWithIgnoredValue( "testvalue" ) + static class AnnotationWithIgnoredValueTestClass {} + + @Test + public void testAnnotationWithIgnoredValueParsing() throws Exception { + ReportModelBuilder modelBuilder = new ReportModelBuilder(); + List tags = modelBuilder.toTags( AnnotationWithIgnoredValueTestClass.class.getAnnotations()[0] ); + assertThat( tags ).hasSize( 1 ); + Tag tag = tags.get( 0 ); + assertThat( tag.getName() ).isEqualTo( "AnnotationWithIgnoredValue" ); + assertThat( tag.getValues() ).isEmpty(); + assertThat( tag.toIdString() ).isEqualTo( "AnnotationWithIgnoredValue" ); + } + @IsTag @Retention( RetentionPolicy.RUNTIME ) @interface AnnotationWithArray { @@ -233,4 +273,33 @@ public void abstract_steps_should_appear_in_the_report_model() throws Throwable StepModel step = getScenario().getModel().getFirstStepModelOfLastScenario(); assertThat( step.words.get( 0 ).getFormattedValue() ).isEqualTo( "abstract step" ); } + + @IsTag + @Retention( RetentionPolicy.RUNTIME ) + @interface ParentTag {} + + @IsTag + @Retention( RetentionPolicy.RUNTIME ) + @interface ParentTagWithValue { + String value(); + } + + @ParentTagWithValue( "SomeValue" ) + @ParentTag + @IsTag + @Retention( RetentionPolicy.RUNTIME ) + @interface TagWithParentTags {} + + @TagWithParentTags + static class AnnotationWithParentTag {} + + @Test + public void testAnnotationWithParentTag() throws Exception { + reportModelBuilder = new ReportModelBuilder(); + List tags = reportModelBuilder.toTags( AnnotationWithParentTag.class.getAnnotations()[0] ); + assertThat( tags ).hasSize( 1 ); + assertThat( tags.get( 0 ).getTags() ).containsAll( Arrays.asList( + "ParentTag", "ParentTagWithValue-SomeValue" ) ); + } + } diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/coffeemachine/ServeCoffeeTest.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/coffeemachine/ServeCoffeeTest.java index bd6b62eb98..1af07c7a96 100644 --- a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/coffeemachine/ServeCoffeeTest.java +++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/coffeemachine/ServeCoffeeTest.java @@ -6,7 +6,7 @@ import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.jgiven.annotation.Description; -import com.tngtech.jgiven.examples.annotation.Order; +import com.tngtech.jgiven.examples.tags.Order; import com.tngtech.jgiven.examples.coffeemachine.steps.GivenCoffee; import com.tngtech.jgiven.examples.coffeemachine.steps.ThenCoffee; import com.tngtech.jgiven.examples.coffeemachine.steps.WhenCoffee; diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/AnotherExampleSubCategory.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/AnotherExampleSubCategory.java new file mode 100644 index 0000000000..fd8cb6c768 --- /dev/null +++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/AnotherExampleSubCategory.java @@ -0,0 +1,15 @@ +package com.tngtech.jgiven.examples.tags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.tngtech.jgiven.annotation.IsTag; + +/** + * Defines a tag that + */ +@ExampleCategory +@CategoryWithValue( "Another Category" ) +@IsTag +@Retention( RetentionPolicy.RUNTIME ) +public @interface AnotherExampleSubCategory {} diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/CategoryWithValue.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/CategoryWithValue.java new file mode 100644 index 0000000000..89ace1cedf --- /dev/null +++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/CategoryWithValue.java @@ -0,0 +1,15 @@ +package com.tngtech.jgiven.examples.tags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.tngtech.jgiven.annotation.IsTag; + +/** + * Demonstrates that category tags can have values + */ +@IsTag( prependType = false ) +@Retention( RetentionPolicy.RUNTIME ) +public @interface CategoryWithValue { + String value(); +} diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleCategory.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleCategory.java new file mode 100644 index 0000000000..f0a2332779 --- /dev/null +++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleCategory.java @@ -0,0 +1,13 @@ +package com.tngtech.jgiven.examples.tags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.tngtech.jgiven.annotation.IsTag; + +/** + * Defines a tag that is used as a category + */ +@IsTag +@Retention( RetentionPolicy.RUNTIME ) +public @interface ExampleCategory {} diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleSubCategory.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleSubCategory.java new file mode 100644 index 0000000000..079a0d8fa6 --- /dev/null +++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleSubCategory.java @@ -0,0 +1,15 @@ +package com.tngtech.jgiven.examples.tags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.tngtech.jgiven.annotation.IsTag; + +/** + * Defines a tag that + */ +@ExampleCategory +@CategoryWithValue( "Some Category" ) +@IsTag +@Retention( RetentionPolicy.RUNTIME ) +public @interface ExampleSubCategory {} diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/annotation/Order.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/Order.java similarity index 86% rename from jgiven-examples/src/test/java/com/tngtech/jgiven/examples/annotation/Order.java rename to jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/Order.java index 011c64ca41..88125ebc31 100644 --- a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/annotation/Order.java +++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/Order.java @@ -1,4 +1,4 @@ -package com.tngtech.jgiven.examples.annotation; +package com.tngtech.jgiven.examples.tags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/TagHierarchyExampleTest.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/TagHierarchyExampleTest.java new file mode 100644 index 0000000000..3c7cb3e73e --- /dev/null +++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/TagHierarchyExampleTest.java @@ -0,0 +1,46 @@ +package com.tngtech.jgiven.examples.tags; + +import com.tngtech.jgiven.junit.SimpleScenarioTest; +import org.junit.Test; + +/** + * This example shows how hierarchical tags work. + *

    + * Hierarchical tags can be created by just annotating a tag annotation with another tag. + * The other tag becomes a parent tag or category tag. This makes it possible to structure + * tags hierarchically. It is even possible to form multiple hierarchies/categories so that a + * tag is contained multiple categories. + *

    + */ +public class TagHierarchyExampleTest extends SimpleScenarioTest { + + @ExampleSubCategory + @Test + public void tags_can_form_a_hierarchy() { + given().tags_annotated_with_tags(); + when().the_report_is_generated(); + then().the_tags_appear_in_a_hierarchy(); + } + + @AnotherExampleSubCategory + @Test + public void parent_tags_can_have_values() { + given().tags_annotated_with_tags_that_have_values(); + when().the_report_is_generated(); + then().the_tags_appear_in_a_hierarchy(); + } + + public static class Steps { + void tags_annotated_with_tags() { + } + + void the_report_is_generated() { + } + + void the_tags_appear_in_a_hierarchy() { + } + + void tags_annotated_with_tags_that_have_values() { + } + } +} diff --git a/jgiven-html5-report/README.md b/jgiven-html5-report/README.md index e6a1b080a3..31647d665f 100644 --- a/jgiven-html5-report/README.md +++ b/jgiven-html5-report/README.md @@ -5,11 +5,3 @@ Uses the power of AngularJS, Foundation, and Font Awesome ## Advantages compared to the static HTML report * It requires less space, because only a single JSONP file has to be generated that is used as input instead of many HTML files for each tag and class * It is dynamic ;-), so expect that there will be additional features in the HTML5 report that are not available in the static one - -## Currently missing features compared to the static HTML report (TODO) - -1. Overview Page - -## Planned features that are not present in the static HTML report - -1. Filtering by multiple tags instead of only one tag diff --git a/jgiven-html5-report/package.json b/jgiven-html5-report/package.json index 009aa4bf5c..501266782c 100644 --- a/jgiven-html5-report/package.json +++ b/jgiven-html5-report/package.json @@ -1,6 +1,6 @@ { "name": "jgiven-html5-report", - "version": "0.6.0", + "version": "0.7.4", "description": "An HTML5 report for JGiven", "main": "src/app/app.js", "devDependencies": { diff --git a/jgiven-html5-report/src/app/app.js b/jgiven-html5-report/src/app/app.js deleted file mode 100644 index 326299d66d..0000000000 --- a/jgiven-html5-report/src/app/app.js +++ /dev/null @@ -1,1118 +0,0 @@ -'use strict'; - -var jgivenReportApp = angular.module('jgivenReportApp', ['ngSanitize','mm.foundation','mm.foundation.offcanvas', - 'chart.js','LocalStorageModule']) - .config(['localStorageServiceProvider', function(localStorageServiceProvider){ - localStorageServiceProvider.setPrefix('jgiven'); - }]) - - - -jgivenReportApp.filter('encodeUri', function ($window) { - return $window.encodeURIComponent; -}); - -jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $timeout, $sanitize, $location, $window, localStorageService) { - $scope.scenarios = []; - $scope.classNameScenarioMap = {}; - $scope.rootPackage = { packages: [] }; - $scope.packageMap = {}; - $scope.tagScenarioMap = {}; // lazy calculated by getTags() - $scope.allTags; - $scope.tags; - $scope.currentPage; - $scope.jgivenReport = jgivenReport; - $scope.nav = {}; - $scope.bookmarks = []; - - $scope.init = function() { - $scope.rootPackage = getRootPackage(); - $scope.allTags = groupTagsByType(getTags()); - $scope.tags = $scope.allTags; - - $scope.bookmarks = localStorageService.get('bookmarks') || []; - $scope.$watch('bookmarks', function () { - localStorageService.set('bookmarks', $scope.bookmarks); - }, true); - - $scope.showSummaryPage(); - }; - - $scope.showSummaryPage = function() { - var scenarios = getAllScenarios(); - - $scope.currentPage = { - title: "Welcome", - breadcrumbs: [''], - scenarios: [], - groupedScenarios: [], - statistics: $scope.gatherStatistics(scenarios), - summary: true - }; - } - - $scope.$on('$locationChangeSuccess', function(event) { - if ($scope.updatingLocation) { - $scope.updatingLocation = false; - return; - } - var search = $location.search(); - var selectedOptions = getOptionsFromSearch( search ); - var part = $location.path().split('/'); - console.log("Location change: " +part); - if (part[1] === '') { - $scope.showSummaryPage(); - } else if (part[1] === 'tag') { - var tag = $scope.tagScenarioMap[ getTagKey({ - name: part[2], - value: part[3] - })].tag; - $scope.updateCurrentPageToTag( tag, selectedOptions ); - } else if (part[1] === 'class') { - $scope.updateCurrentPageToClassName(part[2], selectedOptions); - } else if (part[1] === 'package') { - $scope.updateCurrentPageToPackage( part[2], selectedOptions); - } else if (part[1] === 'scenario') { - $scope.showScenario(part[2],part[3], selectedOptions); - } else if (part[1] === 'all') { - $scope.showAllScenarios( selectedOptions); - } else if (part[1] === 'failed') { - $scope.showFailedScenarios( selectedOptions); - } else if (part[1] === 'pending') { - $scope.showPendingScenarios( selectedOptions); - } else if (part[1] === 'search') { - $scope.search(part[2], selectedOptions); - } - - - $scope.currentPage.embed = search.embed; - $scope.currentPage.print = search.print; - - }); - - function getOptionsFromSearch( search ) { - var result = {}; - result.sort = search.sort; - result.group = search.group; - result.tags = search.tags ? search.tags.split(";") : []; - result.classes = search.classes ? search.classes.split(";") : []; - result.status = search.status ? search.status.split(";") : []; - return result; - } - - $scope.toggleBookmark = function () { - if ($scope.isBookmarked()) { - $scope.removeCurrentBookmark(); - } else { - var name = $scope.currentPage.title; - if (name === 'Search Results') { - name = $scope.currentPage.description; - } - - $scope.bookmarks.push({ - name: name, - url: $window.location.hash, - search: $window.location.search - }); - } - }; - - $scope.removeCurrentBookmark = function() { - $scope.removeBookmark( $scope.findBookmarkIndex() ); - }; - - $scope.removeBookmark = function (index) { - $scope.bookmarks.splice(index, 1); - }; - - $scope.isBookmarked = function() { - return $scope.findBookmarkIndex() !== -1; - }; - - $scope.findBookmarkIndex = function() { - for (var i = 0; i < $scope.bookmarks.length; i++) { - if ($scope.bookmarks[i].url === $location.path()) { - return i; - } - } - return -1; - } - - $scope.togglePackage = function togglePackage( packageObj ) { - packageObj.expanded = !packageObj.expanded - - // recursively open all packages that only have a single subpackage - if (packageObj.classes.length === 0 && packageObj.packages.length === 1) { - togglePackage( packageObj.packages[0]); - } - } - - $scope.currentPath = function() { - return $location.path(); - } - - $scope.updateCurrentPageToClassName = function(className, options) { - $scope.updateCurrentPageToTestCase( $scope.classNameScenarioMap[className], options ); - } - - $scope.updateCurrentPageToPackage = function(packageName, options) { - $scope.currentPage = { - scenarios: [], - subtitle: "Package", - title: packageName, - breadcrumbs: packageName.split("."), - loading: true - }; - $timeout(function() { - var packageObj = $scope.packageMap[ packageName ]; - var scenarios = []; - collectScenariosFromPackage( packageObj, scenarios ); - $scope.currentPage.scenarios = scenarios; - $scope.currentPage.loading = false; - $scope.currentPage.options = getOptions($scope.currentPage.scenarios, options); - $scope.applyOptions(); - }, 0); - } - - function collectScenariosFromPackage( packageObj, scenarios ) { - _.forEach( packageObj.classes, function( clazz ) { - scenarios.pushArray( $scope.classNameScenarioMap[clazz.packageName + "." + clazz.className].scenarios ); - }); - - _.forEach( packageObj.packages, function ( subpackage ) { - collectScenariosFromPackage( subpackage, scenarios ); - }); - } - - $scope.updateCurrentPageToTestCase = function (testCase, options) { - var className = splitClassName(testCase.className); - var scenarios = sortByDescription(testCase.scenarios); - $scope.currentPage = { - scenarios: scenarios, - subtitle: className.packageName, - title: className.className, - breadcrumbs: className.packageName.split("."), - options: getOptions( scenarios, options ) - }; - $scope.applyOptions(); - }; - - $scope.updateCurrentPageToTag = function(tag, options) { - var key = getTagKey(tag); - var scenarios = sortByDescription( $scope.tagScenarioMap[key].scenarios ); - console.log("Update current page to tag "+key); - $scope.currentPage = { - scenarios: scenarios, - title: tag.value ? (tag.prependType ? tag.name + '-' : '') + tag.value : tag.name, - subtitle: tag.value && !tag.prependType ? tag.name : undefined, - description: tag.description, - breadcrumbs: ['TAGS',tag.name,tag.value], - options: getOptions(scenarios, options) - }; - $scope.applyOptions(); - }; - - $scope.showScenario = function( className, methodName, options ) { - var scenarios = sortByDescription(_.filter($scope.classNameScenarioMap[className].scenarios, function(x) { - return x.testMethodName === methodName; - })); - $scope.currentPage = { - scenarios: scenarios, - title: scenarios[0].description.capitalize(), - subtitle: className, - breadcrumbs: ['SCENARIO'].concat(className.split('.')).concat([methodName]), - options: getOptions(scenarios, options) - }; - $scope.applyOptions(); - } - - $scope.showAllScenarios = function( options ) { - $scope.currentPage = { - scenarios: [], - title: 'All Scenarios', - breadcrumbs: ['ALL SCENARIOS'], - loading: true - } - - $timeout(function() { - $scope.currentPage.scenarios = sortByDescription(getAllScenarios()); - $scope.currentPage.loading = false; - $scope.currentPage.options = getOptions($scope.currentPage.scenarios, options); - $scope.applyOptions(); - }, 0); - }; - - $scope.showPendingScenarios = function( options ) { - var pendingScenarios = getPendingScenarios(); - var description = getDescription( pendingScenarios.length, "pending"); - $scope.currentPage = { - scenarios: pendingScenarios, - title: "Pending Scenarios", - description: description, - breadcrumbs: ['PENDING SCENARIOS'], - options: getOptions(pendingScenarios, options) - }; - $scope.applyOptions(); - }; - - $scope.applyOptions = function applyOptions() { - var page = $scope.currentPage; - var selectedSortOption = getSelectedSortOption(page); - var filteredSorted = selectedSortOption.apply( - _.filter( page.scenarios, getFilterFunction( page )) ); - page.groupedScenarios = getSelectedGroupOption( page ).apply( filteredSorted ); - page.statistics = $scope.gatherStatistics( filteredSorted ); - page.filtered = page.scenarios.length - filteredSorted.length; - $scope.updateLocationSearchOptions(); - } - - $scope.updateLocationSearchOptions = function updateLocationSearchOptions() { - $scope.updatingLocation = true; - var selectedSortOption = getSelectedSortOption( $scope.currentPage ); - $location.search('sort', selectedSortOption.default ? null : selectedSortOption.id); - - var selectedGroupOption = getSelectedGroupOption($scope.currentPage); - $location.search('group', selectedGroupOption.default ? null : selectedGroupOption.id); - - var selectedTags = getSelectedOptions( $scope.currentPage.options.tagOptions ); - $location.search('tags', selectedTags.length > 0 ? _.map(selectedTags, 'name').join(";") : null); - - var selectedStatus = getSelectedOptions( $scope.currentPage.options.statusOptions ); - $location.search('status', selectedStatus.length > 0 ? _.map(selectedStatus, 'id').join(";") : null); - - var selectedClasses = getSelectedOptions( $scope.currentPage.options.classOptions ); - $location.search('classes', selectedClasses.length > 0 ? _.map(selectedClasses, 'name').join(";") : null); - - $scope.updatingLocation = false; - } - - $scope.showFailedScenarios = function( options ) { - var failedScenarios = getFailedScenarios(); - var description = getDescription( failedScenarios.length, "failed"); - $scope.currentPage = { - scenarios: failedScenarios, - title: "Failed Scenarios", - description: description, - breadcrumbs: ['FAILED SCENARIOS'], - options: getOptions(failedScenarios, options) - }; - $scope.applyOptions(); - }; - - function getDescription( count, status ) { - if (count === 0) { - return "There are no " + status +" scenarios. Keep rocking!"; - } else if (count === 1) { - return "There is only 1 "+status+" scenario. You nearly made it!"; - } else { - return "There are " + count + " " + status +" scenarios"; - } - } - - $scope.toggleTagType = function(tagType) { - tagType.collapsed = !tagType.collapsed; - }; - - $scope.toggleScenario = function(scenario) { - scenario.expanded = !scenario.expanded; - }; - - $scope.searchSubmit = function() { - console.log("Searching for " + $scope.nav.search); - - var x = $location.path("search/" + $scope.nav.search); - } - - $scope.search = function search(searchString) { - console.log("Searching for "+searchString); - - $scope.currentPage = { - scenarios: [], - title: "Search Results", - description: "Searched for '" + searchString + "'", - breadcrumbs: ['Search', searchString ], - loading: true - }; - - $timeout( function() { - $scope.currentPage.scenarios = $scope.findScenarios(searchString); - $scope.currentPage.loading = false; - $scope.currentPage.options = getDefaultOptions( $scope.currentPage.scenarios ); - $scope.applyOptions(); - },1); - } - - $scope.gatherStatistics = function gatherStatistics( scenarios ) { - var statistics = { - count: scenarios.length, - failed: 0, - pending: 0, - success: 0, - totalNanos: 0 - }; - - _.forEach( scenarios, function(x) { - statistics.totalNanos += x.durationInNanos; - if (x.executionStatus === 'SUCCESS') { - statistics.success++; - } else if (x.executionStatus === 'FAILED') { - statistics.failed++; - } else { - statistics.pending++; - } - }); - - $timeout( function() { - statistics.chartData = [statistics.success, statistics.failed, statistics.pending]; - }, 0); - - return statistics; - } - - $scope.findScenarios = function findScenarios( searchString ) { - var searchStrings = searchString.split(" "); - console.log("Searching for "+searchStrings); - - var regexps = _.map(searchStrings, function(x) { - return new RegExp(x, "i"); - } ); - - return sortByDescription(_.filter( getAllScenarios(), function(x) { - return scenarioMatchesAll(x, regexps); - } )); - } - - $scope.printCurrentPage = function printCurrentPage() { - $location.search("print",true); - $timeout(printPage,0); - }; - - function printPage() { - if ($scope.currentPage.loading) { - $timeout(printPage, 0); - } else { - window.print(); - $timeout(function() { - $location.search("print", null); - },0); - } - } - - $scope.expandAll = function expandAll() { - _.forEach($scope.currentPage.scenarios, function(x) { - x.expanded = true; - }); - }; - - $scope.collapseAll = function collapseAll() { - _.forEach($scope.currentPage.scenarios, function(x) { - x.expanded = false; - }); - }; - - $scope.sortOptionSelected = function sortOptionSelected( sortOption ) { - deselectAll( $scope.currentPage.options.sortOptions ); - sortOption.selected = true; - $scope.applyOptions(); - }; - - $scope.groupOptionSelected = function groupOptionSelected( groupOption ) { - deselectAll( $scope.currentPage.options.groupOptions ); - groupOption.selected = true; - $scope.applyOptions(); - }; - - $scope.filterOptionSelected = function filterOptionSelected( filterOption ) { - filterOption.selected = !filterOption.selected; - $scope.applyOptions(); - }; - - function ownProperties( obj ) { - var result = new Array(); - for (var p in obj) { - if (obj.hasOwnProperty(p)) { - result.push(p); - } - } - return result; - } - - function getFilterFunction( page ) { - - var anyStatusMatches = anyOptionMatches(getSelectedOptions(page.options.statusOptions)); - var anyTagMatches = anyOptionMatches(getSelectedOptions(page.options.tagOptions)); - var anyClassMatches = anyOptionMatches(getSelectedOptions(page.options.classOptions)); - - return function( scenario ) { - return anyStatusMatches( scenario ) && anyTagMatches( scenario ) && anyClassMatches( scenario ); - } - } - - function anyOptionMatches( filterOptions ) { - // by default nothing is filtered - if (filterOptions.length === 0) { - return function () { - return true; - }; - } - - return function( scenario ) { - for (var i = 0; i < filterOptions.length; i++) { - if (filterOptions[i].apply( scenario )) { - return true; - } - } - return false; - } - } - - function getSelectedSortOption( page ) { - return getSelectedOptions( page.options.sortOptions)[0]; - } - - function getSelectedGroupOption( page ) { - return getSelectedOptions( page.options.groupOptions)[0]; - } - - function getSelectedOptions( options ) { - return _.filter( options, 'selected'); - } - - function deselectAll( options ) { - _.forEach( options, function( option ) { - option.selected = false; - }); - } - - - function scenarioMatchesAll( scenario, regexpList ) { - for (var i = 0; i < regexpList.length; i++ ) { - - if (!scenarioMatches(scenario, regexpList[i])) { - return false; - } - } - return true; - } - - function scenarioMatches( scenario, regexp ) { - if (scenario.className.match(regexp)) { - return true; - } - - if (scenario.description.match(regexp)) { - return true; - } - - for (var i = 0; i < scenario.tags.length; i++) { - var tag = scenario.tags[i]; - if ( (tag.name && tag.name.match(regexp)) || - (tag.value && tag.value.match(regexp))) { - return true; - } - } - - for (var i = 0; i < scenario.scenarioCases.length; i++) { - if (caseMatches( scenario.scenarioCases[i], regexp )) { - return true; - } - } - - } - - function caseMatches( scenarioCase, regexp) { - for (var i = 0; i < scenarioCase.steps.length; i++) { - if (stepMatches(scenarioCase.steps[i], regexp)) { - return true; - } - } - - return false; - } - - function stepMatches( step, regexp ) { - for (var i = 0; i < step.words.length; i++) { - if (step.words[i].value.match(regexp)) { - return true; - } - } - - return false; - } - - function groupTagsByType(tagList) { - var types = {}; - _.forEach(tagList, function(x) { - var list = types[x.name]; - if (!list) { - list = new Array(); - types[x.name] = list; - } - list.push(x); - }) - return _.map(_.sortBy(Object.keys(types), function(key) { - return key; - }), function(x) { - return { - type: x, - tags: types[x] - } - }); - } - - function getAllScenarios() { - return _.flatten( _.map( jgivenReport.scenarios, function(x) { - return x.scenarios; - }), true); - } - - function getPendingScenarios() { - return getScenariosWhere( function(x) { - return x.executionStatus !== "FAILED" && x.executionStatus !== "SUCCESS"; - }); - } - - function getFailedScenarios() { - return getScenariosWhere( function(x) { - return x.executionStatus === "FAILED"; - }); - } - - function getScenariosWhere( filter ) { - return sortByDescription(_.filter( getAllScenarios(), filter )); - } - - function sortByDescription( scenarios ) { - var scenarios = _.forEach(_.sortBy(scenarios, function(x) { - return x.description.toLowerCase(); - }), function(x) { - x.expanded = false; - }); - - // directly expand a scenario if it is the only one - if (scenarios.length === 1) { - scenarios[0].expanded = true; - } - - return scenarios; - } - - function getRootPackage() { - var allScenarios = jgivenReport.scenarios; - var packageMap = {}; // maps full qualified package name to package object - var rootPackage = getPackage(""); - var classObj; - - for (var i = 0; i < allScenarios.length; i++ ) { - classObj = splitClassName( allScenarios[i].className ); - classObj.index = i; - $scope.classNameScenarioMap[allScenarios[i].className] = allScenarios[i]; - - getPackage( classObj.packageName).classes.push(classObj); - - } - $scope.packageMap = packageMap; - - return rootPackage; - - function getPackage( packageName ) { - var parentPackage, index, simpleName; - var packageObj = packageMap[ packageName ]; - if (packageObj === undefined) { - index = packageName.lastIndexOf('.'); - simpleName = packageName.substr(index + 1); - - packageObj = { - qualifiedName: packageName, - name: simpleName, - classes: [], - packages: [] - } - packageMap[ packageName ] = packageObj; - - if (simpleName !== "") { - parentPackage = getPackage(packageName.substring(0, index)); - parentPackage.packages.push(packageObj); - } - } - return packageObj; - } - } - - function getTagKey(tag) { - return tag.name + '-' + tag.value; - } - - function getTags() { - var res = {}; - var key; - var tagEntry; - _.forEach(jgivenReport.scenarios, function(testCase) { - _.forEach(testCase.scenarios, function(scenario) { - _.forEach(scenario.tags, function(tag) { - key = getTagKey(tag); - res[ key ] = tag; - tagEntry = $scope.tagScenarioMap[ key ]; - if (!tagEntry) { - tagEntry = { - tag: tag, - scenarios: new Array() - }; - $scope.tagScenarioMap[ key ] = tagEntry - } - tagEntry.scenarios.push(scenario); - }); - }); - }); - - return _.sortBy(_.values(res), getTagKey); - } - - function getOptions( scenarios, optionSelection ) { - var result = getDefaultOptions( scenarios ); - - if (optionSelection.sort) { - deselectAll( result.sortOptions ); - selectOption( 'id', optionSelection.sort, result.sortOptions ); - } - - if (optionSelection.group) { - deselectAll( result.groupOptions ); - selectOption( 'id', optionSelection.group, result.groupOptions ); - } - - if (optionSelection.tags) { - deselectAll( result.tagOptions ); - _.forEach( optionSelection.tags, function( tagName ) { - selectOption( 'name', tagName, result.tagOptions ); - }); - } - - if (optionSelection.status) { - deselectAll( result.statusOptions ); - _.forEach( optionSelection.status, function( status ) { - selectOption( 'id', status, result.statusOptions ); - }); - } - - if (optionSelection.classes) { - deselectAll( result.classOptions ); - _.forEach( optionSelection.classes, function( className ) { - selectOption( 'name', className, result.classOptions ); - }); - } - return result; - } - - function selectOption( property, value, options ) { - _.filter( options, function( option ) { - return option[property] === value; - })[0].selected = true; - } - - function getDefaultOptions( scenarios ) { - var uniqueSortedTags = getUniqueSortedTags( scenarios); - - return { - sortOptions: getDefaultSortOptions( uniqueSortedTags ), - groupOptions: getDefaultGroupOptions(), - statusOptions: getDefaultStatusOptions( ), - tagOptions: getDefaultTagOptions( uniqueSortedTags ), - classOptions: getDefaultClassOptions( scenarios ) - } - } - - function getDefaultStatusOptions( ) { - return [ - { - selected: false, - name: 'Successful', - id: 'success', - apply: function( scenario ) { - return scenario.executionStatus === 'SUCCESS'; - } - }, - { - selected: false, - name: 'Failed', - id: 'fail', - apply: function( scenario ) { - return scenario.executionStatus === 'FAILED'; - } - }, - { - selected: false, - name: 'Pending', - id: 'pending', - apply: function( scenario ) { - return scenario.executionStatus !== 'SUCCESS' && - scenario.executionStatus !== 'FAILED'; - } - }, - ]; - } - - function getDefaultTagOptions( uniqueSortedTags ) { - var result = new Array(); - _.forEach( uniqueSortedTags, function( tag ) { - var tagName = tagToString(tag); - result.push( { - selected: false, - name: tagName, - apply: function( scenario ) { - for (var i = 0; i < scenario.tags.length; i++) { - if (tagToString(scenario.tags[i]) === tagName) { - return true; - } - } - return false; - } - }) - }); - return result; - } - - function getDefaultClassOptions( scenarios ) { - var uniqueSortedClassNames = getUniqueSortedClassNames( scenarios) - , result = new Array(); - _.forEach( uniqueSortedClassNames, function( className ) { - result.push( { - selected: false, - name: className, - apply: function( scenario ) { - return scenario.className === className; - } - }) - }); - return result; - } - - function getUniqueSortedClassNames( scenarios ) { - var allClasses = {}; - _.forEach( scenarios, function( scenario ) { - allClasses[ scenario.className ] = true; - }); - return ownProperties(allClasses).sort(); - } - - function getUniqueSortedTags( scenarios ) { - var allTags = {}; - _.forEach( scenarios, function( scenario ) { - _.forEach( scenario.tags, function( tag ) { - allTags[ tagToString( tag )] = tag; - }); - }); - return _.map( ownProperties(allTags).sort(), function( tagName ) { - return allTags[ tagName ]; - }); - } - - function getDefaultGroupOptions() { - return [ - { - selected: true, - default: true, - id: 'none', - name: 'None', - apply: function( scenarios ) { - var result = toArrayOfGroups({ - 'all': scenarios - }); - result[0].expanded = true; - return result; - } - }, - { - selected: false, - id: 'class', - name: 'Class', - apply: function( scenarios ) { - return toArrayOfGroups(_.groupBy( scenarios, 'className' )); - } - }, - { - selected: false, - id: 'status', - name: 'Status', - apply: function( scenarios ) { - return toArrayOfGroups(_.groupBy( scenarios, function( scenario ) { - return getReadableExecutionStatus( scenario.executionStatus ); - } )); - } - }, - { - selected: false, - id: 'tag', - name: 'Tag', - apply: function( scenarios ) { - return toArrayOfGroups( groupByTag( scenarios )); - } - } - ]; - } - - function groupByTag( scenarios ) { - var result = {}, i, j, tagName; - _.forEach(scenarios, function( scenario ) { - _.forEach( scenario.tags, function( tag ) { - tagName = tagToString(tag); - addToArrayProperty( result, tagName, scenario); - }); - - if (scenario.tags.length === 0) { - // extra space to ensure that it is first in the list - addToArrayProperty( result, ' No Tag', scenario); - } - }); - return result; - } - - function addToArrayProperty( obj, p, value ) { - if (!obj.hasOwnProperty( p )) { - obj[ p ] = new Array(); - } - obj[ p ].push(value); - } - - function getReadableExecutionStatus( status ) { - switch (status ) { - case 'SUCCESS': return 'Successful'; - case 'FAILED': return 'Failed'; - default: return 'Pending'; - } - } - - function getDefaultSortOptions( uniqueSortedTags ) { - var result= [ - { - selected: true, - default: true, - id: 'name-asc', - name: 'A-Z', - apply: function( scenarios ) { - return _.sortBy(scenarios, function(x) { - return x.description.toLowerCase(); - }); - } - }, - { - selected: false, - id: 'name-desc', - name: 'Z-A', - apply: function( scenarios ) { - return _.chain( scenarios ).sortBy( function(x) { - return x.description.toLowerCase(); - }).reverse().value(); - } - }, - { - selected: false, - id: 'status-asc', - name: 'Failed', - apply: function( scenarios ) { - return _.chain( scenarios).sortBy( 'executionStatus' ) - .value(); - } - }, - { - selected: false, - id: 'status-desc', - name: 'Successful', - apply: function( scenarios ) { - return _.chain( scenarios).sortBy( 'executionStatus' ) - .reverse().value(); - } - }, - { - selected: false, - id: 'duration-asc', - name: 'Fastest', - apply: function( scenarios ) { - return _.sortBy(scenarios, 'durationInNanos'); - } - }, - { - selected: false, - id: 'duration-desc', - name: 'Slowest', - apply: function( scenarios ) { - return _.chain( scenarios).sortBy( 'durationInNanos' ) - .reverse().value(); - } - }, - - ]; - - return result.concat( getTagSortOptions( uniqueSortedTags )) - } - - function getTagSortOptions( uniqueSortedTags ) { - var result = new Array(); - - var tagTypes = groupTagsByType( uniqueSortedTags ); - - _.forEach( tagTypes, function( tagType ) { - if (tagType.tags.length > 1) { - result.push( { - selected: false, - name: tagType.type, - apply: function( scenarios ) { - return _.sortBy( scenarios, function( scenario ) { - var x = getTagOfType( scenario.tags, tagType.type )[0]; - return x ? x.value : undefined; - }); - } - } ); - } - }); - - return result; - } - - function getTagOfType( tags, type ) { - return _.filter( tags, function( tag ) { - return tag.name === type; - }); - } - - function toArrayOfGroups( obj ) { - var result = new Array(); - _.forEach( ownProperties(obj), function(p) { - result.push( { - name: p, - values: obj[p] - }); - }); - return _.sortBy(result, 'name'); - } - - $scope.nanosToSeconds = function( nanos ) { - var secs = nanos / 1000000000; - var res = parseFloat(secs).toFixed(3); - return res; - }; - - $scope.tagToString = tagToString; - - function tagToString(tag) { - var res = ''; - - if (!tag.value || tag.prependType) { - res = tag.name; - } - - if (tag.value) { - if (res) { - res += '-'; - } - res += tag.value; - } - - return res; - }; - - $scope.getCssClassOfTag = function getCssClassOfTag( tag ) { - if (tag.cssClass) { - return tag.cssClass; - } - return 'tag-' + tag.name; - }; - - /** - * Returns the content of style attribute for the given tag - */ - $scope.getStyleOfTag = function getStyleOfTag( tag ) { - if (tag.color) { - return 'background-color: '+tag.color; - } - return ''; - }; - - $scope.isHeaderCell = function( rowIndex, columnIndex, headerType ) { - console.log(headerType); - if (rowIndex === 0 && (headerType === 'HORIZONTAL' || headerType === 'BOTH')) { - return true; - } - if (columnIndex === 0 && (headerType === 'VERTICAL' || headerType === 'BOTH')) { - return true; - } - return false; - }; - - /** - * Returns all but the intro words of the given array of words. - * It is assumed that only the first word can be an intro word - * @param words the array of all non-intro words of a step - */ - $scope.getNonIntroWords = function getNonIntroWords( words ) { - if (words[0].isIntroWord) { - return words.slice(1); - } - return words; - }; - - $scope.init(); - -}); - -jgivenReportApp.controller('SummaryCtrl', function ($scope) { - var red = Chart.defaults.global.colours[2]; - var blue = Chart.defaults.global.colours[0]; - var green = { - fillColor: 'rgba(0,150,0,0.5)', - strokeColor: 'rgba(0,150,0,0.7)', - pointColor: "rgba(0,150,0,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(0,150,0,0.8)" - }; - var gray = Chart.defaults.global.colours[6]; - - $scope.labels = ['Successful', 'Failed', 'Pending']; - $scope.colours = [green, red, gray]; - $scope.options = { - percentageInnerCutout : 60, - animationEasing : "easeInOutCubic", - animationSteps : 50, - segmentShowStroke: false - }; - -}); - - -String.prototype.capitalize = function() { - return this.charAt(0).toUpperCase() + this.slice(1); -}; - -Array.prototype.pushArray = function(arr) { - this.push.apply(this, arr); -}; - -function splitClassName( fullQualifiedClassName ) { - var index = fullQualifiedClassName.lastIndexOf('.'); - var className = fullQualifiedClassName.substr(index+1); - var packageName = fullQualifiedClassName.substr(0,index); - return { - className: className, - packageName: packageName - }; -} - -var jgivenReport = { - scenarios: new Array(), - - setMetaData: function setMetaData(metaData) { - this.metaData = metaData; - _.forEach(metaData.data, function(x) { - document.writeln(""); - }); - }, - - addScenarios: function addScenarios(scenarios) { - this.scenarios = this.scenarios.concat(scenarios); - }, - - setAllScenarios: function setAllScenarios(allScenarios) { - this.scenarios = allScenarios; - } -}; diff --git a/jgiven-html5-report/src/app/css/jgivenreport.css b/jgiven-html5-report/src/app/css/jgivenreport.css index ad378c39d2..7862b0cad1 100644 --- a/jgiven-html5-report/src/app/css/jgivenreport.css +++ b/jgiven-html5-report/src/app/css/jgivenreport.css @@ -28,12 +28,11 @@ h5 { .header-fixed { position: fixed; z-index: 1000; - top:0; - left:0; - right:0; + top: 0; + left: 0; + right: 0; } - .tag.hidden { display: none; } @@ -59,8 +58,8 @@ h5 { } .expand-icon:hover, .collapse-icon:hover, .print-icon:hover, - .scenario:hover .scenario-link-icon:hover, - .remove-bookmark-icon:hover, .add-bookmark-icon:hover { +.scenario:hover .scenario-link-icon:hover, +.remove-bookmark-icon:hover, .add-bookmark-icon:hover { color: #008cba; } @@ -87,7 +86,6 @@ h5 { } - /** * Ugly workaround to fix an issue with foundation where * the drop-down menu is positioned outside of window @@ -123,7 +121,6 @@ h5 { * <--- Ugly workaround end */ - .scenario-group-header { margin-bottom: 10px; margin-top: 20px; @@ -228,8 +225,8 @@ h5 { table.steps { border: none; border-spacing: 0; - padding:0; - margin:0; + padding: 0; + margin: 0; margin-bottom: 0.5rem; } @@ -387,7 +384,7 @@ input.search { position: relative; background: white; padding-bottom: 5rem; - max-width:100%; + max-width: 100%; min-height: 100%; height: 100%; } @@ -446,16 +443,15 @@ ul.tags { margin-left: 0.8rem; } -.side-nav li a:not(.button){ +.side-nav li a:not(.button) { padding: 0.5rem 1.8rem; } -.side-nav li li a:not(.button){ +.side-nav li li a:not(.button) { padding-top: 0.1rem; padding-bottom: 0.1rem; } - .side-nav, .side-nav ul { line-height: 1.2; } @@ -478,7 +474,6 @@ li.subheading { margin-top: 0.2rem; } - li.heading ul { text-transform: none; } @@ -495,24 +490,31 @@ li.subheading > ul { margin-top: 0; } -.open-package-icon { - color: #eee; +/** + * Navigation trees + */ + +i.transparent { + color: transparent; } -div.package-node { - position: relative; +.open-tree-node-icon { + color: #eee; } -div.package-node:hover .open-package-icon { - color: inherit; +div.tree-node { + position: relative; } -div.package-node:hover { +div.tree-node:hover { background-color: #eee; } +div.tree-node:hover .open-tree-node-icon { + color: inherit; +} -a.show-package-link { +a.show-tree-node-link { display: inline !important; position: absolute; right: 1.8rem; @@ -520,11 +522,11 @@ a.show-package-link { padding: 0 !important; } -div.package-node:hover a { +div.tree-node:hover a { background: none !important; } -a.show-package-link:hover { +a.show-tree-link:hover { background: none !important; } @@ -548,8 +550,8 @@ a.show-package-link:hover { top: 76px; bottom: 0; left: 0; - right:0; - background-color: rgba(255,255,255,0.8); + right: 0; + background-color: rgba(255, 255, 255, 0.8); z-index: 2000; } @@ -566,7 +568,7 @@ a.show-package-link:hover { } .off-canvas-wrap { - overflow: auto; + overflow: auto; } .page-statistics { @@ -591,7 +593,6 @@ a.show-package-link:hover { padding-left: 0; } - @media only screen and (max-width: 64em) { .scenario-container { margin-left: 0; @@ -607,19 +608,31 @@ a.show-package-link:hover { @media only screen and (min-width: 64.063em) { .tab-bar { - display:none; + display: none; } } @media only screen and (min-width: 64.063em) and (max-width: 90em) { - .scenario-container { margin-left: 15rem; } - .sidebar { width: 15rem; } + .scenario-container { + margin-left: 15rem; + } + + .sidebar { + width: 15rem; + } } @media only screen and (min-width: 90.063em) and (max-width: 120em) { - .scenario-container { margin-left: 21rem; } - .sidebar { width: 21rem; } + .scenario-container { + margin-left: 21rem; + } + + .sidebar { + width: 21rem; + } } /* avoid showing angularjs expressions on page loading */ -[ng\:cloak],[ng-cloak],.ng-cloak{display:none !important} +[ng\:cloak], [ng-cloak], .ng-cloak { + display: none !important +} diff --git a/jgiven-html5-report/src/app/index.html b/jgiven-html5-report/src/app/index.html index eed5814198..9a83e13dc8 100644 --- a/jgiven-html5-report/src/app/index.html +++ b/jgiven-html5-report/src/app/index.html @@ -1,298 +1,343 @@ - - - - JGiven Report - - - - - - - - - - - - - - - + + + + JGiven Report + + + + + + + + + + + + + + + -
    +
    -
    +
    - + -
    +
    - - + -
    +
    - + - - + - + +
    + +

    Loading

    +
    + + + + +
    +
    + +
    +
    + - +
    -
    + - +
    +
    +

    {{currentPage.subtitle}}

    -
    -
    -

    {{currentPage.subtitle}}

    -

    {{currentPage.title}}

    -

    -
    +

    {{currentPage.title}}

    - +

    +
    -
    -
    - -
    -
    + -
    - - - - -
    +
    +
    +
    +
    -

    Loading

    +
    + + + + +
    +
    - +

    Loading

    -
    + - +
    -
    - - {{currentPage.statistics.success}} Successful, - {{currentPage.statistics.failed}} Failed, - {{currentPage.statistics.pending}} Pending, - {{currentPage.statistics.count}} Total ({{nanosToSeconds(currentPage.statistics.totalNanos)}}s) - ({{ currentPage.filtered }} Filtered) -
    + - +
    + + {{currentPage.statistics.success}} Successful, + {{currentPage.statistics.failed}} Failed, + {{currentPage.statistics.pending}} Pending, + {{currentPage.statistics.count}} Total ({{nanosToSeconds(currentPage.statistics.totalNanos)}}s) + ({{ currentPage.filtered }} Filtered) +
    - + + +
    +
    - + -
    +
    -
    +
    -

    +

    {{scenarioGroup.name}} {{scenarioGroup.values.length}}

    @@ -302,63 +347,88 @@

    +
    - +

    {{scenario.description.capitalize()}} - {{scenario.scenarioCases.length}} - - FAILED - PENDING + {{scenario.scenarioCases.length}} + + FAILED + PENDING ({{ nanosToSeconds(scenario.durationInNanos) }}s)

    -
    -
    -
    - Case {{case.caseNr}}: + +
    +
    +
    + Case + {{case.caseNr}}: - {{param}} = {{case.explicitArguments[$index]}}, + {{param}} = {{case.explicitArguments[$index]}},
    -
    + +
    - +
    {{step.words[0].isIntroWord ? step.words[0].value.capitalize() : ''}} + {{step.words[0].isIntroWord ? + step.words[0].value.capitalize() : ''}} + - + <{{word.argumentInfo.parameterName}}> - {{ word.argumentInfo.formattedValue || word.value }} + {{ word.argumentInfo.formattedValue || word.value }} + ng-repeat="value in row track by $index">{{ value }} +
    {{ value }}
    - + - {{ scenarioCase.caseNr }} + {{ scenarioCase.caseNr }} @@ -367,14 +437,18 @@
    - + - ({{ nanosToSeconds(step.durationInNanos) }}s) + ({{ nanosToSeconds(step.durationInNanos) }}s)
    -
    FAILED: {{case.errorMessage}}
    +
    FAILED: {{case.errorMessage}} +
    @@ -383,15 +457,22 @@
    Cases
    - + - - + +
    #{{param}} + {{param}} + Status
    {{case.caseNr}}{{arg}} + {{arg}} + -
    {{case.errorMessage}}
    +
    + {{case.errorMessage}} +
    @@ -400,44 +481,58 @@
    Cases
    - +
    -
    -
    - -
    + - - +

    - - - - - - - - - - - - - + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jgiven-html5-report/src/app/lib/app.js b/jgiven-html5-report/src/app/lib/app.js new file mode 100644 index 0000000000..49fe3ebdc4 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/app.js @@ -0,0 +1,22 @@ +'use strict'; + +var jgivenReportApp = angular.module('jgivenReportApp', ['ngSanitize', 'mm.foundation', 'mm.foundation.offcanvas', + 'chart.js', 'LocalStorageModule']) + .config(['localStorageServiceProvider', function (localStorageServiceProvider) { + localStorageServiceProvider.setPrefix('jgiven'); + }]); + + +jgivenReportApp.filter('encodeUri', function ($window) { + return $window.encodeURIComponent; +}); + + +String.prototype.capitalize = function () { + return this.charAt(0).toUpperCase() + this.slice(1); +}; + +Array.prototype.pushArray = function (arr) { + this.push.apply(this, arr); +}; + diff --git a/jgiven-html5-report/src/app/lib/chartCtrl.js b/jgiven-html5-report/src/app/lib/chartCtrl.js new file mode 100644 index 0000000000..cea4fe822c --- /dev/null +++ b/jgiven-html5-report/src/app/lib/chartCtrl.js @@ -0,0 +1,25 @@ + +jgivenReportApp.controller('ChartCtrl', function ($scope) { + var red = Chart.defaults.global.colours[2]; + var blue = Chart.defaults.global.colours[0]; + var green = { + fillColor: 'rgba(0,150,0,0.5)', + strokeColor: 'rgba(0,150,0,0.7)', + pointColor: "rgba(0,150,0,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(0,150,0,0.8)" + }; + var gray = Chart.defaults.global.colours[6]; + + $scope.labels = ['Successful', 'Failed', 'Pending']; + $scope.colours = [green, red, gray]; + $scope.options = { + percentageInnerCutout: 60, + animationEasing: "easeInOutCubic", + animationSteps: 50, + segmentShowStroke: false + }; + +}); + diff --git a/jgiven-html5-report/src/app/lib/classService.js b/jgiven-html5-report/src/app/lib/classService.js new file mode 100644 index 0000000000..1aea428c36 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/classService.js @@ -0,0 +1,142 @@ +/** + * Responsible for handling class and package-related operations, e.g. finding scenarios for a certain class or package + */ + +jgivenReportApp.factory('classService', ['dataService', function (dataService) { + 'use strict'; + + var classNameScenarioMap = getClassNameScenarioMap(); + var packageNodeMap = {}; + var rootPackage = getRootPackage(); + + + function getClassNameScenarioMap() { + var classNameScenarioMap = {}; + var testClasses = dataService.getTestClasses(); + + _.forEach(testClasses, function (testClass) { + classNameScenarioMap[testClass.className] = testClass; + }); + return classNameScenarioMap; + } + + function getTestClasses() { + return dataService.getTestClasses(); + } + + /** + * Builds up the navigation tree for classes + */ + function getRootPackage() { + var allClasses = getTestClasses(); + var rootPackage = getPackageNode(""); + + _.forEach(allClasses, function (testClass) { + var classObj = splitClassName(testClass.className); + getPackageNode(classObj.packageName).addClassNode(createClassNode(classObj)); + }); + + return rootPackage; + + function createPackageNode(packageObj) { + return { + packageObj: packageObj, + + nodeName: function () { + return packageObj.name; + }, + + url: function () { + return '#package/' + packageObj.qualifiedName; + }, + + leafs: function () { + return packageObj.classes; + }, + + childNodes: function () { + return packageObj.packages; + }, + + hasChildren: function () { + return packageObj.packages.length + packageObj.classes.length > 0; + }, + + addClassNode: function (classNode) { + packageObj.classes.push(classNode); + }, + + addPackageNode: function (packageNode) { + packageObj.packages.push(packageNode); + } + } + } + + function createClassNode(classObj) { + return { + fullQualifiedName: function () { + return classObj.packageName + "." + classObj.className; + }, + + nodeName: function () { + return classObj.className; + }, + + url: function () { + return '#class/' + this.fullQualifiedName(); + } + }; + } + + function getPackageNode(packageName) { + var parentPackage, index, simpleName; + var packageNode = packageNodeMap[packageName]; + if (packageNode === undefined) { + index = packageName.lastIndexOf('.'); + simpleName = packageName.substr(index + 1); + + packageNode = createPackageNode({ + qualifiedName: packageName, + name: simpleName, + classes: [], + packages: [] + }); + + packageNodeMap[packageName] = packageNode; + + if (simpleName !== "") { + parentPackage = getPackageNode(packageName.substring(0, index)); + parentPackage.addPackageNode(packageNode); + } + } + return packageNode; + } + } + + function getScenariosOfPackage(packageName) { + var scenarios = []; + var packageNode = packageNodeMap[packageName]; + collectScenariosFromPackage(packageNode, scenarios); + return scenarios; + } + + function collectScenariosFromPackage(packageNode, scenarios) { + _.forEach(packageNode.leafs(), function (clazzNode) { + scenarios.pushArray(classNameScenarioMap[clazzNode.fullQualifiedName()].scenarios); + }); + + _.forEach(packageNode.childNodes(), function (subpackageNode) { + collectScenariosFromPackage(subpackageNode, scenarios); + }); + } + + + return { + getTestClasses: getTestClasses, + getScenariosOfPackage: getScenariosOfPackage, + getRootPackage: function () { + return rootPackage; + } + }; + +}]); \ No newline at end of file diff --git a/jgiven-html5-report/src/app/lib/dataService.js b/jgiven-html5-report/src/app/lib/dataService.js new file mode 100644 index 0000000000..d7a8ea1465 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/dataService.js @@ -0,0 +1,87 @@ +/** + * Provides functions to access the generated scenario data + */ + +jgivenReportApp.factory('dataService', [function () { + 'use strict'; + + var tagFile = jgivenReport.tagFile; + var scenarios = jgivenReport.scenarios; + + function getAllScenarios() { + return _.flatten(_.map(scenarios, function (x) { + return x.scenarios; + }), true); + } + + function getScenariosWhere(filter) { + return sortByDescription(_.filter(getAllScenarios(), filter)); + } + + function getPendingScenarios() { + return getScenariosWhere(function (x) { + return x.executionStatus !== "FAILED" && x.executionStatus !== "SUCCESS"; + }); + } + + function getFailedScenarios() { + return getScenariosWhere(function (x) { + return x.executionStatus === "FAILED"; + }); + } + + function getTagByTagId(tagId) { + var tagInstance = tagFile.tags[tagId]; + var tagType = tagFile.tagTypeMap[tagInstance.tagType]; + var tag = Object.create(tagType); + tag.value = tagInstance.value; + return tag; + } + + return { + + getTagFile: function () { + return tagFile; + }, + + getScenarios: function () { + return scenarios; + }, + + getTestClasses: function () { + return scenarios; + }, + + getAllScenarios: getAllScenarios, + getPendingScenarios: getPendingScenarios, + getFailedScenarios: getFailedScenarios, + getTagByTagId: getTagByTagId + + }; +}]); + +/** + * Global variable that is used by the generated JSONP files + */ +var jgivenReport = { + scenarios: [], + + setTags: function setTags(tagFile) { + this.tagFile = tagFile; + }, + + setMetaData: function setMetaData(metaData) { + this.metaData = metaData; + _.forEach(metaData.data, function (x) { + document.writeln(""); + }); + }, + + addScenarios: function addScenarios(scenarios) { + this.scenarios = this.scenarios.concat(scenarios); + }, + + setAllScenarios: function setAllScenarios(allScenarios) { + this.scenarios = allScenarios; + } +}; diff --git a/jgiven-html5-report/src/app/lib/navigationCtrl.js b/jgiven-html5-report/src/app/lib/navigationCtrl.js new file mode 100644 index 0000000000..378dfcdf05 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/navigationCtrl.js @@ -0,0 +1,20 @@ +/** + * AngularJS controller to handle the navigation tree on the left + */ + + +jgivenReportApp.controller('JGivenNavigationCtrl', function ($scope, classService, tagService) { + 'use strict'; + + /** + * The root tag node of the hierarchical tag tree + */ + $scope.rootTags = tagService.getRootTags(); + + /** + * The root package node of the hierarchical package tree + */ + $scope.rootPackage = classService.getRootPackage(); + + +}); diff --git a/jgiven-html5-report/src/app/lib/optionService.js b/jgiven-html5-report/src/app/lib/optionService.js new file mode 100644 index 0000000000..48a8bcc999 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/optionService.js @@ -0,0 +1,358 @@ +/** + * Provides functions for sorting, grouping and filtering + */ + +jgivenReportApp.factory('optionService', ['dataService', function (dataService) { + 'use strict'; + + function groupTagsByType(tagList) { + var types = {}; + _.forEach(tagList, function (x) { + var list = types[x.name]; + if (!list) { + list = []; + types[x.name] = list; + } + list.push(x); + }); + return _.map(_.sortBy(Object.keys(types), function (key) { + return key; + }), function (x) { + return { + type: x, + tags: types[x] + } + }); + } + + + function getOptions(scenarios, optionSelection) { + var result = getDefaultOptions(scenarios); + + if (optionSelection.sort) { + deselectAll(result.sortOptions); + selectOption('id', optionSelection.sort, result.sortOptions); + } + + if (optionSelection.group) { + deselectAll(result.groupOptions); + selectOption('id', optionSelection.group, result.groupOptions); + } + + if (optionSelection.tags) { + deselectAll(result.tagOptions); + _.forEach(optionSelection.tags, function (tagName) { + selectOption('name', tagName, result.tagOptions); + }); + } + + if (optionSelection.status) { + deselectAll(result.statusOptions); + _.forEach(optionSelection.status, function (status) { + selectOption('id', status, result.statusOptions); + }); + } + + if (optionSelection.classes) { + deselectAll(result.classOptions); + _.forEach(optionSelection.classes, function (className) { + selectOption('name', className, result.classOptions); + }); + } + return result; + } + + function selectOption(property, value, options) { + _.filter(options, function (option) { + return option[property] === value; + })[0].selected = true; + } + + function getDefaultOptions(scenarios) { + var uniqueSortedTags = getUniqueSortedTags(scenarios); + + return { + sortOptions: getDefaultSortOptions(uniqueSortedTags), + groupOptions: getDefaultGroupOptions(), + statusOptions: getDefaultStatusOptions(), + tagOptions: getDefaultTagOptions(uniqueSortedTags), + classOptions: getDefaultClassOptions(scenarios) + } + } + + function getDefaultStatusOptions() { + return [ + { + selected: false, + name: 'Successful', + id: 'success', + apply: function (scenario) { + return scenario.executionStatus === 'SUCCESS'; + } + }, + { + selected: false, + name: 'Failed', + id: 'fail', + apply: function (scenario) { + return scenario.executionStatus === 'FAILED'; + } + }, + { + selected: false, + name: 'Pending', + id: 'pending', + apply: function (scenario) { + return scenario.executionStatus !== 'SUCCESS' && + scenario.executionStatus !== 'FAILED'; + } + } + ]; + } + + function getDefaultTagOptions(uniqueSortedTags) { + var result = []; + _.forEach(uniqueSortedTags, function (tag) { + var tagName = tagToString(tag); + result.push({ + selected: false, + name: tagName, + apply: function (scenario) { + for (var i = 0; i < scenario.tags.length; i++) { + if (tagToString(scenario.tags[i]) === tagName) { + return true; + } + } + return false; + } + }) + }); + return result; + } + + function getDefaultClassOptions(scenarios) { + var uniqueSortedClassNames = getUniqueSortedClassNames(scenarios) + , result = []; + _.forEach(uniqueSortedClassNames, function (className) { + result.push({ + selected: false, + name: className, + apply: function (scenario) { + return scenario.className === className; + } + }) + }); + return result; + } + + function getUniqueSortedClassNames(scenarios) { + var allClasses = {}; + _.forEach(scenarios, function (scenario) { + allClasses[scenario.className] = true; + }); + return ownProperties(allClasses).sort(); + } + + function getUniqueSortedTags(scenarios) { + var allTags = {}; + _.forEach(scenarios, function (scenario) { + _.forEach(scenario.tags, function (tag) { + allTags[tagToString(tag)] = tag; + }); + }); + return _.map(ownProperties(allTags).sort(), function (tagName) { + return allTags[tagName]; + }); + } + + function getDefaultGroupOptions() { + return [ + { + selected: true, + default: true, + id: 'none', + name: 'None', + apply: function (scenarios) { + var result = toArrayOfGroups({ + 'all': scenarios + }); + result[0].expanded = true; + return result; + } + }, + { + selected: false, + id: 'class', + name: 'Class', + apply: function (scenarios) { + return toArrayOfGroups(_.groupBy(scenarios, 'className')); + } + }, + { + selected: false, + id: 'status', + name: 'Status', + apply: function (scenarios) { + return toArrayOfGroups(_.groupBy(scenarios, function (scenario) { + return getReadableExecutionStatus(scenario.executionStatus); + })); + } + }, + { + selected: false, + id: 'tag', + name: 'Tag', + apply: function (scenarios) { + return toArrayOfGroups(groupByTag(scenarios)); + } + } + ]; + } + + function groupByTag(scenarios) { + var result = {}, i, j, tagName; + _.forEach(scenarios, function (scenario) { + _.forEach(scenario.tags, function (tag) { + tagName = tagToString(tag); + addToArrayProperty(result, tagName, scenario); + }); + + if (scenario.tags.length === 0) { + // extra space to ensure that it is first in the list + addToArrayProperty(result, ' No Tag', scenario); + } + }); + return result; + } + + function addToArrayProperty(obj, p, value) { + if (!obj.hasOwnProperty(p)) { + obj[p] = []; + } + obj[p].push(value); + } + + function getDefaultSortOptions(uniqueSortedTags) { + var result = [ + { + selected: true, + default: true, + id: 'name-asc', + name: 'A-Z', + apply: function (scenarios) { + return _.sortBy(scenarios, function (x) { + return x.description.toLowerCase(); + }); + } + }, + { + selected: false, + id: 'name-desc', + name: 'Z-A', + apply: function (scenarios) { + return _.chain(scenarios).sortBy(function (x) { + return x.description.toLowerCase(); + }).reverse().value(); + } + }, + { + selected: false, + id: 'status-asc', + name: 'Failed', + apply: function (scenarios) { + return _.chain(scenarios).sortBy('executionStatus') + .value(); + } + }, + { + selected: false, + id: 'status-desc', + name: 'Successful', + apply: function (scenarios) { + return _.chain(scenarios).sortBy('executionStatus') + .reverse().value(); + } + }, + { + selected: false, + id: 'duration-asc', + name: 'Fastest', + apply: function (scenarios) { + return _.sortBy(scenarios, 'durationInNanos'); + } + }, + { + selected: false, + id: 'duration-desc', + name: 'Slowest', + apply: function (scenarios) { + return _.chain(scenarios).sortBy('durationInNanos') + .reverse().value(); + } + } + + ]; + + return result.concat(getTagSortOptions(uniqueSortedTags)) + } + + function getTagSortOptions(uniqueSortedTags) { + var result = []; + + var tagTypes = groupTagsByType(uniqueSortedTags); + + _.forEach(tagTypes, function (tagType) { + if (tagType.tags.length > 1) { + result.push({ + selected: false, + name: tagType.type, + apply: function (scenarios) { + return _.sortBy(scenarios, function (scenario) { + var x = getTagOfType(scenario.tags, tagType.type)[0]; + return x ? x.value : undefined; + }); + } + }); + } + }); + + return result; + } + + function getTagOfType(tags, type) { + return _.filter(tags, function (tag) { + return getTagName(tag) === type; + }); + } + + function toArrayOfGroups(obj) { + var result = []; + _.forEach(ownProperties(obj), function (p) { + result.push({ + name: p, + values: obj[p] + }); + }); + return _.sortBy(result, 'name'); + } + + function getOptionsFromSearch(search) { + var result = {}; + result.sort = search.sort; + result.group = search.group; + result.tags = search.tags ? search.tags.split(";") : []; + result.classes = search.classes ? search.classes.split(";") : []; + result.status = search.status ? search.status.split(";") : []; + return result; + } + + + return { + getOptions: getOptions, + getOptionsFromSearch: getOptionsFromSearch, + getDefaultOptions: getDefaultOptions + + }; + +}]) +; \ No newline at end of file diff --git a/jgiven-html5-report/src/app/lib/reportCtrl.js b/jgiven-html5-report/src/app/lib/reportCtrl.js new file mode 100644 index 0000000000..78724657b2 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/reportCtrl.js @@ -0,0 +1,518 @@ +/** + * Main controller + */ + +jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $timeout, $sanitize, $location, $window, localStorageService, + dataService, tagService, classService, searchService, optionService) { + + /** + * The current list of shown scenarios + */ + $scope.scenarios = []; + + /** + * Maps full qualified class names to lists of scenarios + */ + $scope.classNameScenarioMap = {}; + + /** + * Maps full qualified package names to package node objects + */ + $scope.packageNodeMap = {}; + + $scope.currentPage = {}; + $scope.jgivenReport = jgivenReport; + $scope.nav = {}; + $scope.bookmarks = []; + + $scope.init = function () { + + $scope.bookmarks = localStorageService.get('bookmarks') || []; + $scope.$watch('bookmarks', function () { + localStorageService.set('bookmarks', $scope.bookmarks); + }, true); + + $scope.showSummaryPage(); + }; + + var getAllScenarios = dataService.getAllScenarios; + + + $scope.showSummaryPage = function () { + var scenarios = getAllScenarios(); + + $scope.currentPage = { + title: "Welcome", + breadcrumbs: [''], + scenarios: [], + groupedScenarios: [], + statistics: $scope.gatherStatistics(scenarios), + summary: true + }; + }; + + $scope.$on('$locationChangeSuccess', function (event) { + if ($scope.updatingLocation) { + $scope.updatingLocation = false; + return; + } + var search = $location.search(); + var selectedOptions = optionService.getOptionsFromSearch(search); + var part = $location.path().split('/'); + console.log("Location change: " + part); + if (part[1] === '') { + $scope.showSummaryPage(); + } else if (part[1] === 'tag') { + if (part[3]) { + var tag = tagService.getTagByKey(getTagKey({ + name: part[2], + value: part[3] + })); + $scope.updateCurrentPageToTag(tag, selectedOptions); + } else { + var tagNameNode = tagService.getTagNameNode(part[2]); + $scope.updateCurrentPageToTagNameNode(tagNameNode, selectedOptions); + } + } else if (part[1] === 'class') { + $scope.updateCurrentPageToClassName(part[2], selectedOptions); + } else if (part[1] === 'package') { + $scope.updateCurrentPageToPackage(part[2], selectedOptions); + } else if (part[1] === 'scenario') { + $scope.showScenario(part[2], part[3], selectedOptions); + } else if (part[1] === 'all') { + $scope.showAllScenarios(selectedOptions); + } else if (part[1] === 'failed') { + $scope.showFailedScenarios(selectedOptions); + } else if (part[1] === 'pending') { + $scope.showPendingScenarios(selectedOptions); + } else if (part[1] === 'search') { + $scope.search(part[2], selectedOptions); + } + + + $scope.currentPage.embed = search.embed; + $scope.currentPage.print = search.print; + + }); + + $scope.toggleBookmark = function () { + if ($scope.isBookmarked()) { + $scope.removeCurrentBookmark(); + } else { + var name = $scope.currentPage.title; + if (name === 'Search Results') { + name = $scope.currentPage.description; + } + + $scope.bookmarks.push({ + name: name, + url: $window.location.hash, + search: $window.location.search + }); + } + }; + + $scope.removeCurrentBookmark = function () { + $scope.removeBookmark($scope.findBookmarkIndex()); + }; + + $scope.removeBookmark = function (index) { + $scope.bookmarks.splice(index, 1); + }; + + $scope.isBookmarked = function () { + return $scope.findBookmarkIndex() !== -1; + }; + + $scope.findBookmarkIndex = function () { + for (var i = 0; i < $scope.bookmarks.length; i++) { + if ($scope.bookmarks[i].url === $location.path()) { + return i; + } + } + return -1; + }; + + $scope.toggleTreeNode = function toggleTreeNode(node) { + node.expanded = !node.expanded; + + // recursively open all packages that only have a single subpackage + if (node.leafs().length === 0 && node.childNodes().length === 1) { + toggleTreeNode(node.childNodes()[0]); + } + }; + + $scope.currentPath = function () { + return $location.path(); + }; + + $scope.updateCurrentPageToClassName = function (className, options) { + $scope.updateCurrentPageToTestCase($scope.classNameScenarioMap[className], options); + }; + + $scope.updateCurrentPageToPackage = function (packageName, options) { + $scope.currentPage = { + scenarios: [], + subtitle: "Package", + title: packageName, + breadcrumbs: packageName.split("."), + loading: true + }; + $timeout(function () { + var scenarios = classService.getScenariosOfPackage(packageName); + $scope.currentPage.scenarios = scenarios; + $scope.currentPage.loading = false; + $scope.currentPage.options = optionService.getOptions($scope.currentPage.scenarios, options); + $scope.applyOptions(); + }, 0); + }; + + $scope.updateCurrentPageToTestCase = function (testCase, options) { + var className = splitClassName(testCase.className); + var scenarios = sortByDescription(testCase.scenarios); + $scope.currentPage = { + scenarios: scenarios, + subtitle: className.packageName, + title: className.className, + breadcrumbs: className.packageName.split("."), + options: optionService.getOptions(scenarios, options) + }; + $scope.applyOptions(); + }; + + $scope.updateCurrentPageToTag = function (tag, options) { + var key = getTagKey(tag); + var scenarios = sortByDescription(tagService.getScenariosByTag(tag)); + console.log("Update current page to tag " + key); + $scope.currentPage = { + scenarios: scenarios, + title: tag.value ? (tag.prependType ? getTagName(tag) + '-' : '') + tag.value : getTagName(tag), + subtitle: tag.value && !tag.prependType ? getTagName(tag) : undefined, + description: tag.description, + breadcrumbs: ['TAGS', getTagName(tag), tag.value], + options: optionService.getOptions(scenarios, options) + }; + $scope.applyOptions(); + }; + + $scope.updateCurrentPageToTagNameNode = function (tagNameNode, options) { + var scenarios = sortByDescription(tagNameNode.scenarios()); + $scope.currentPage = { + scenarios: scenarios, + title: tagNameNode.nodeName(), + description: "", + breadcrumbs: ['TAGS', tagNameNode.nodeName() + ], + options: optionService.getOptions(scenarios, options) + }; + $scope.applyOptions(); + }; + + $scope.showScenario = function (className, methodName, options) { + var scenarios = sortByDescription(_.filter($scope.classNameScenarioMap[className].scenarios, function (x) { + return x.testMethodName === methodName; + })); + $scope.currentPage = { + scenarios: scenarios, + title: scenarios[0].description.capitalize(), + subtitle: className, + breadcrumbs: ['SCENARIO'].concat(className.split('.')).concat([methodName]), + options: optionService.getOptions(scenarios, options) + }; + $scope.applyOptions(); + }; + + $scope.showAllScenarios = function (options) { + $scope.currentPage = { + scenarios: [], + title: 'All Scenarios', + breadcrumbs: ['ALL SCENARIOS'], + loading: true + }; + + $timeout(function () { + $scope.currentPage.scenarios = sortByDescription(getAllScenarios()); + $scope.currentPage.loading = false; + $scope.currentPage.options = optionService.getOptions($scope.currentPage.scenarios, options); + $scope.applyOptions(); + }, 0); + }; + + $scope.showPendingScenarios = function (options) { + var pendingScenarios = dataService.getPendingScenarios(); + var description = getDescription(pendingScenarios.length, "pending"); + $scope.currentPage = { + scenarios: pendingScenarios, + title: "Pending Scenarios", + description: description, + breadcrumbs: ['PENDING SCENARIOS'], + options: optionService.getOptions(pendingScenarios, options) + }; + $scope.applyOptions(); + }; + + $scope.applyOptions = function applyOptions() { + var page = $scope.currentPage; + var selectedSortOption = getSelectedSortOption(page); + var filteredSorted = selectedSortOption.apply( + _.filter(page.scenarios, getFilterFunction(page))); + page.groupedScenarios = getSelectedGroupOption(page).apply(filteredSorted); + page.statistics = $scope.gatherStatistics(filteredSorted); + page.filtered = page.scenarios.length - filteredSorted.length; + $scope.updateLocationSearchOptions(); + }; + + $scope.updateLocationSearchOptions = function updateLocationSearchOptions() { + $scope.updatingLocation = true; + var selectedSortOption = getSelectedSortOption($scope.currentPage); + $location.search('sort', selectedSortOption.default ? null : selectedSortOption.id); + + var selectedGroupOption = getSelectedGroupOption($scope.currentPage); + $location.search('group', selectedGroupOption.default ? null : selectedGroupOption.id); + + var selectedTags = getSelectedOptions($scope.currentPage.options.tagOptions); + $location.search('tags', selectedTags.length > 0 ? _.map(selectedTags, 'name').join(";") : null); + + var selectedStatus = getSelectedOptions($scope.currentPage.options.statusOptions); + $location.search('status', selectedStatus.length > 0 ? _.map(selectedStatus, 'id').join(";") : null); + + var selectedClasses = getSelectedOptions($scope.currentPage.options.classOptions); + $location.search('classes', selectedClasses.length > 0 ? _.map(selectedClasses, 'name').join(";") : null); + + $scope.updatingLocation = false; + }; + + $scope.showFailedScenarios = function (options) { + var failedScenarios = dataService.getFailedScenarios(); + var description = getDescription(failedScenarios.length, "failed"); + $scope.currentPage = { + scenarios: failedScenarios, + title: "Failed Scenarios", + description: description, + breadcrumbs: ['FAILED SCENARIOS'], + options: optionService.getOptions(failedScenarios, options) + }; + $scope.applyOptions(); + }; + + function getDescription(count, status) { + if (count === 0) { + return "There are no " + status + " scenarios. Keep rocking!"; + } else if (count === 1) { + return "There is only 1 " + status + " scenario. You nearly made it!"; + } else { + return "There are " + count + " " + status + " scenarios"; + } + } + + $scope.toggleTagType = function (tagType) { + tagType.collapsed = !tagType.collapsed; + }; + + $scope.toggleScenario = function (scenario) { + scenario.expanded = !scenario.expanded; + }; + + $scope.searchSubmit = function () { + console.log("Searching for " + $scope.nav.search); + + $location.path("search/" + $scope.nav.search); + }; + + $scope.search = function search(searchString) { + console.log("Searching for " + searchString); + + $scope.currentPage = { + scenarios: [], + title: "Search Results", + description: "Searched for '" + searchString + "'", + breadcrumbs: ['Search', searchString], + loading: true + }; + + $timeout(function () { + $scope.currentPage.scenarios = searchService.findScenarios(searchString); + $scope.currentPage.loading = false; + $scope.currentPage.options = optionService.getDefaultOptions($scope.currentPage.scenarios); + $scope.applyOptions(); + }, 1); + }; + + $scope.gatherStatistics = function gatherStatistics(scenarios) { + var statistics = { + count: scenarios.length, + failed: 0, + pending: 0, + success: 0, + totalNanos: 0 + }; + + _.forEach(scenarios, function (x) { + statistics.totalNanos += x.durationInNanos; + if (x.executionStatus === 'SUCCESS') { + statistics.success++; + } else if (x.executionStatus === 'FAILED') { + statistics.failed++; + } else { + statistics.pending++; + } + }); + + $timeout(function () { + statistics.chartData = [statistics.success, statistics.failed, statistics.pending]; + }, 0); + + return statistics; + }; + + $scope.printCurrentPage = function printCurrentPage() { + $location.search("print", true); + $timeout(printPage, 0); + }; + + function printPage() { + if ($scope.currentPage.loading) { + $timeout(printPage, 0); + } else { + window.print(); + $timeout(function () { + $location.search("print", null); + }, 0); + } + } + + $scope.expandAll = function expandAll() { + _.forEach($scope.currentPage.scenarios, function (x) { + x.expanded = true; + }); + }; + + $scope.collapseAll = function collapseAll() { + _.forEach($scope.currentPage.scenarios, function (x) { + x.expanded = false; + }); + }; + + $scope.sortOptionSelected = function sortOptionSelected(sortOption) { + deselectAll($scope.currentPage.options.sortOptions); + sortOption.selected = true; + $scope.applyOptions(); + }; + + $scope.groupOptionSelected = function groupOptionSelected(groupOption) { + deselectAll($scope.currentPage.options.groupOptions); + groupOption.selected = true; + $scope.applyOptions(); + }; + + $scope.filterOptionSelected = function filterOptionSelected(filterOption) { + filterOption.selected = !filterOption.selected; + $scope.applyOptions(); + }; + + function getFilterFunction(page) { + + var anyStatusMatches = anyOptionMatches(getSelectedOptions(page.options.statusOptions)); + var anyTagMatches = anyOptionMatches(getSelectedOptions(page.options.tagOptions)); + var anyClassMatches = anyOptionMatches(getSelectedOptions(page.options.classOptions)); + + return function (scenario) { + return anyStatusMatches(scenario) && anyTagMatches(scenario) && anyClassMatches(scenario); + } + } + + function anyOptionMatches(filterOptions) { + // by default nothing is filtered + if (filterOptions.length === 0) { + return function () { + return true; + }; + } + + return function (scenario) { + for (var i = 0; i < filterOptions.length; i++) { + if (filterOptions[i].apply(scenario)) { + return true; + } + } + return false; + } + } + + function getSelectedSortOption(page) { + return getSelectedOptions(page.options.sortOptions)[0]; + } + + function getSelectedGroupOption(page) { + return getSelectedOptions(page.options.groupOptions)[0]; + } + + function getSelectedOptions(options) { + return _.filter(options, 'selected'); + } + + + $scope.nanosToSeconds = function (nanos) { + var secs = nanos / 1000000000; + return parseFloat(secs).toFixed(3); + }; + + $scope.tagIdToString = function tagIdToString(tagId) { + var tag = tagService.getTagByTagId(tagId); + return tagToString(tag); + }; + + $scope.tagToString = tagToString; + + $scope.getUrlFromTagId = function getUrlFromTagId(tagId) { + var tag = $scope.getTagByTagId(tagId); + return '#tag/' + getTagName(tag) + '/' + $window.encodeURIComponent(tag.value); + }; + + $scope.getTagByTagId = function (tagId) { + return tagService.getTagByTagId(tagId); + }; + + $scope.getCssClassOfTag = function getCssClassOfTag(tagId) { + var tag = $scope.getTagByTagId(tagId); + if (tag.cssClass) { + return tag.cssClass; + } + return 'tag-' + getTagName(tag); + }; + + /** + * Returns the content of style attribute for the given tag + */ + $scope.getStyleOfTag = function getStyleOfTag(tagId) { + var tag = tagService.getTagByTagId(tagId); + if (tag.color) { + return 'background-color: ' + tag.color; + } + return ''; + }; + + $scope.isHeaderCell = function (rowIndex, columnIndex, headerType) { + console.log(headerType); + if (rowIndex === 0 && (headerType === 'HORIZONTAL' || headerType === 'BOTH')) { + return true; + } + return columnIndex === 0 && (headerType === 'VERTICAL' || headerType === 'BOTH'); + + }; + + /** + * Returns all but the intro words of the given array of words. + * It is assumed that only the first word can be an intro word + * @param words the array of all non-intro words of a step + */ + $scope.getNonIntroWords = function getNonIntroWords(words) { + if (words[0].isIntroWord) { + return words.slice(1); + } + return words; + }; + + $scope.init(); + +}); diff --git a/jgiven-html5-report/src/app/lib/searchService.js b/jgiven-html5-report/src/app/lib/searchService.js new file mode 100644 index 0000000000..8f7f199a59 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/searchService.js @@ -0,0 +1,81 @@ +/** + * Provides search functionality + */ + +jgivenReportApp.factory('searchService', ['dataService', function (dataService) { + 'use strict'; + + function findScenarios(searchString) { + var searchStrings = searchString.split(" "); + console.log("Searching for " + searchStrings); + + var regexps = _.map(searchStrings, function (x) { + return new RegExp(x, "i"); + }); + + return sortByDescription(_.filter(dataService.getAllScenarios(), function (x) { + return scenarioMatchesAll(x, regexps); + })); + } + + function scenarioMatchesAll(scenario, regexpList) { + for (var i = 0; i < regexpList.length; i++) { + + if (!scenarioMatches(scenario, regexpList[i])) { + return false; + } + } + return true; + } + + function scenarioMatches(scenario, regexp) { + if (scenario.className.match(regexp)) { + return true; + } + + if (scenario.description.match(regexp)) { + return true; + } + + var i; + for (i = 0; i < scenario.tags.length; i++) { + var tag = scenario.tags[i]; + if ((getTagName(tag) && getTagName(tag).match(regexp)) || + (tag.value && tag.value.match(regexp))) { + return true; + } + } + + for (i = 0; i < scenario.scenarioCases.length; i++) { + if (caseMatches(scenario.scenarioCases[i], regexp)) { + return true; + } + } + + } + + function caseMatches(scenarioCase, regexp) { + for (var i = 0; i < scenarioCase.steps.length; i++) { + if (stepMatches(scenarioCase.steps[i], regexp)) { + return true; + } + } + + return false; + } + + function stepMatches(step, regexp) { + for (var i = 0; i < step.words.length; i++) { + if (step.words[i].value.match(regexp)) { + return true; + } + } + + return false; + } + + + return { + findScenarios: findScenarios + }; +}]); diff --git a/jgiven-html5-report/src/app/lib/tagService.js b/jgiven-html5-report/src/app/lib/tagService.js new file mode 100644 index 0000000000..83a62b74f1 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/tagService.js @@ -0,0 +1,222 @@ +/** + * Responsible for handling tag-related operations + */ + +jgivenReportApp.factory('tagService', ['dataService', function (dataService) { + 'use strict'; + + /** + * Maps tag IDs to tags and their scenarios + */ + var tagScenarioMap = getTagScenarioMap(dataService.getScenarios()); + + /** + * Maps tag keys to tag nodes + */ + var tagNodeMap = {}; + + /** + * Maps tag names to list of tags with the same name + */ + var tagNameMap = {}; + + + /** + * Goes through all scenarios to find all tags. + * For each tag found, builds up a map of tag keys to tag entries, + * where a tag entry contains the tag definition and the list of scenarios + * that are tagged with that tag + */ + function getTagScenarioMap(scenarios) { + var tagEntry; + var tagScenarioMap = {}; + _.forEach(scenarios, function (testCase) { + _.forEach(testCase.scenarios, function (scenario) { + + scenario.tags = []; + + _.forEach(scenario.tagIds, function (tagId) { + + var tag = addEntry(tagId).tag; + scenario.tags.push(tag); + + function addEntry(tagId) { + var tag = getTagByTagId(tagId); + var tagKey = getTagKey(tag); + tagEntry = tagScenarioMap[tagKey]; + if (!tagEntry) { + tagEntry = { + tag: tag, + scenarios: [] + }; + tagScenarioMap[tagKey] = tagEntry; + } + tagEntry.scenarios.push(scenario); + + _.forEach(tagEntry.tag.tags, function (tagId) { + addEntry(tagId); + }); + + return tagEntry; + } + + }); + }); + }); + return tagScenarioMap; + } + + /** + * Builds up a hierarchy of tag nodes that is shown in the + * navigation and returns the list of root nodes + */ + function getRootTags() { + var rootTags; + + _.forEach(_.values(tagScenarioMap), function (tagEntry) { + var tagNode = getTagNode(tagEntry); + var name = getTagName(tagEntry.tag); + var nameNode = tagNameMap[name]; + if (!nameNode) { + nameNode = createNameNode(name); + tagNameMap[name] = nameNode; + } + nameNode.addTagNode(tagNode); + }); + + rootTags = _.filter(_.values(tagNameMap), function (tagNode) { + return tagNode.subTags().length > 1; + }); + + rootTags = rootTags.concat(_.map(_.filter(_.values(tagNameMap), function (tagNode) { + return tagNode.subTags().length === 1 + && (!tagNode.subTags()[0].tag().tags + || tagNode.subTags()[0].tag().tags.length === 0); + }), function (tagNode) { + return tagNode.subTags()[0]; + })); + + return rootTags; + + function getTagNode(tagEntry) { + var tag = tagEntry.tag; + var key = getTagKey(tag); + var tagNode = tagNodeMap[key]; + if (!tagNode) { + tagNode = createTagNode(tagEntry); + tagNodeMap[key] = tagNode; + if (tag.tags && tag.tags.length > 0) { + _.forEach(tag.tags, function (parentTagId) { + var parentTag = getTagByTagId(parentTagId); + var parentTagEntry = tagScenarioMap[getTagKey(parentTag)]; + var parentTagNode = getTagNode(parentTagEntry); + parentTagNode.addTagNode(tagNode); + }); + } + } + return tagNode; + } + + function createTagNode(tagEntry) { + var tag = tagEntry.tag; + var scenarios = tagEntry.scenarios; + var node = createNode(tagToString(tag)); + + node.url = function () { + return '#tag/' + window.encodeURIComponent(getTagName(tag)) + '/' + window.encodeURIComponent(tag.value); + }; + + node.scenarios = function () { + return scenarios; + }; + + node.tag = function () { + return tag; + }; + + + return node; + } + + function createNameNode(name) { + var node = createNode(name); + + node.url = function () { + return '#tag/' + window.encodeURIComponent(name); + }; + + node.scenarios = function () { + var scenarioMap = {}; + + _.forEach(node.subTags(), function (subTag) { + _.forEach(subTag.scenarios(), function (scenario) { + scenarioMap[getScenarioId(scenario)] = scenario; + }); + }); + + return _.values(scenarioMap); + }; + + return node; + } + + function createNode(name) { + var subTags = []; + return { + + nodeName: function () { + return name; + }, + + leafs: function () { + return _.filter(subTags, function (t) { + return !t.hasChildren(); + }); + }, + + childNodes: function () { + return _.filter(subTags, function (t) { + return t.hasChildren(); + }); + }, + + hasChildren: function () { + return subTags.length > 0; + }, + + addTagNode: function (tagNode) { + subTags.push(tagNode); + }, + + subTags: function () { + return subTags; + } + } + } + } + + function getScenariosByTag(tag) { + return tagScenarioMap[getTagKey(tag)].scenarios; + } + + function getTagByTagId(tagId) { + return dataService.getTagByTagId(tagId); + } + + function getTagByKey(tagKey) { + return tagScenarioMap[tagKey].tag; + } + + function getTagNameNode(name) { + return tagNameMap[name]; + } + + return { + getScenariosByTag: getScenariosByTag, + getTagByTagId: getTagByTagId, + getTagByKey: getTagByKey, + getRootTags: getRootTags, + getTagNameNode: getTagNameNode + }; +}]) +; \ No newline at end of file diff --git a/jgiven-html5-report/src/app/lib/util.js b/jgiven-html5-report/src/app/lib/util.js new file mode 100644 index 0000000000..c0fccc1b9e --- /dev/null +++ b/jgiven-html5-report/src/app/lib/util.js @@ -0,0 +1,85 @@ +/** + * Utility functions + */ + +function getTagName(tag) { + return tag.name ? tag.name : tag.type; +} + +function getTagKey(tag) { + return getTagName(tag) + '-' + tag.value; +} + +function tagToString(tag) { + var res = ''; + + if (!tag.value || tag.prependType) { + res = getTagName(tag); + } + + if (tag.value) { + if (res) { + res += '-'; + } + res += tag.value; + } + + return res; +} + +function splitClassName(fullQualifiedClassName) { + var index = fullQualifiedClassName.lastIndexOf('.'); + var className = fullQualifiedClassName.substr(index + 1); + var packageName = fullQualifiedClassName.substr(0, index); + return { + className: className, + packageName: packageName + }; +} + +function getScenarioId(scenario) { + return scenario.className + "." + scenario.testMethodName; +} + +function sortByDescription(scenarios) { + var sortedScenarios = _.forEach(_.sortBy(scenarios, function (x) { + return x.description.toLowerCase(); + }), function (x) { + x.expanded = false; + }); + + // directly expand a scenario if it is the only one + if (sortedScenarios.length === 1) { + sortedScenarios[0].expanded = true; + } + + return sortedScenarios; +} + +function getReadableExecutionStatus(status) { + switch (status) { + case 'SUCCESS': + return 'Successful'; + case 'FAILED': + return 'Failed'; + default: + return 'Pending'; + } +} + +function ownProperties(obj) { + var result = []; + for (var p in obj) { + if (obj.hasOwnProperty(p)) { + result.push(p); + } + } + return result; +} + +function deselectAll(options) { + _.forEach(options, function (option) { + option.selected = false; + }); +} + diff --git a/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/Html5ReportGenerator.java b/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/Html5ReportGenerator.java index e0cf3ef61d..7a4c4ac2de 100644 --- a/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/Html5ReportGenerator.java +++ b/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/Html5ReportGenerator.java @@ -40,7 +40,8 @@ public void generate() { try { unzipApp( targetDirectory ); createDataFiles(); - generateMetaData( targetDirectory ); + generateMetaData(); + generateTagFile(); } catch( IOException e ) { throw Throwables.propagate( e ); } @@ -60,6 +61,9 @@ public void handleReportModel( ReportModel model, File file ) { caseCountOfCurrentBatch += getCaseCount( model ); + // do not serialize tags as they are serialized separately + model.setTagMap( null ); + new Gson().toJson( model, writer ); writer.append( "," ); @@ -109,17 +113,28 @@ static class MetaData { List data = Lists.newArrayList(); } - private void generateMetaData( File toDir ) throws IOException { - File metaDataFile = new File( toDir, "metaData.js" ); + private void generateMetaData() throws IOException { + File metaDataFile = new File( dataDirectory, "metaData.js" ); log.debug( "Generating " + metaDataFile + "..." ); String content = "jgivenReport.setMetaData(" + new Gson().toJson( metaData ) + " );"; Files.write( content, metaDataFile, Charsets.UTF_8 ); + } + + private void generateTagFile() throws IOException { + File tagFile = new File( dataDirectory, "tags.js" ); + log.debug( "Generating " + tagFile + "..." ); + + TagFile tagFileContent = new TagFile(); + tagFileContent.fill( completeReportModel.getTagIdMap() ); + String content = "jgivenReport.setTags(" + new Gson().toJson( tagFileContent ) + " );"; + + Files.write( content, tagFile, Charsets.UTF_8 ); } - private void unzipApp( File toDir ) throws IOException { + protected void unzipApp( File toDir ) throws IOException { String appZipPath = "/" + Html5ReportGenerator.class.getPackage().getName().replace( '.', '/' ) + "/app.zip"; log.debug( "Unzipping {}...", appZipPath ); diff --git a/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/TagFile.java b/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/TagFile.java new file mode 100644 index 0000000000..7bb7887a41 --- /dev/null +++ b/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/TagFile.java @@ -0,0 +1,33 @@ +package com.tngtech.jgiven.report.html5; + +import java.util.Map; + +import com.google.common.collect.Maps; +import com.tngtech.jgiven.report.model.Tag; + +public class TagFile { + private Map tagTypeMap = Maps.newLinkedHashMap(); + private Map tags = Maps.newLinkedHashMap(); + + private static class TagInstance { + String tagType; + String value; + } + + public void fill( Map tagIdMap ) { + for( Map.Entry entry : tagIdMap.entrySet() ) { + + // remove the value as it is not part of the type + Tag tag = entry.getValue().copy(); + tag.setValue( (String) null ); + tagTypeMap.put( tag.getType(), tag ); + + TagInstance instance = new TagInstance(); + instance.tagType = tag.getType(); + instance.value = entry.getValue().getValueString(); + tags.put( entry.getKey(), instance ); + + } + } + +} diff --git a/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/ScenarioExecutionTest.java b/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/ScenarioExecutionTest.java index 5dca9713c3..e329ae9b33 100644 --- a/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/ScenarioExecutionTest.java +++ b/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/ScenarioExecutionTest.java @@ -20,7 +20,6 @@ import com.tngtech.jgiven.junit.test.ThenTestStep; import com.tngtech.jgiven.junit.test.WhenTestStep; import com.tngtech.jgiven.report.model.AttachmentModel; -import com.tngtech.jgiven.report.model.Tag; @RunWith( DataProviderRunner.class ) @JGivenConfiguration( TestConfiguration.class ) @@ -210,11 +209,11 @@ public void After_methods_are_called_even_if_step_fails() throws Throwable { public void configured_tags_are_reported() throws Throwable { given().something(); getScenario().finished(); - List tags = getScenario().getModel().getLastScenarioModel().getTags(); - assertThat( tags ).isNotEmpty(); - Tag tag = tags.get( 0 ); - assertThat( tag ).isNotNull(); - assertThat( tag.getName() ).isEqualTo( "ConfiguredTag" ); + List tagIds = getScenario().getModel().getLastScenarioModel().getTagIds(); + assertThat( tagIds ).isNotEmpty(); + String tagId = tagIds.get( 0 ); + assertThat( tagId ).isNotNull(); + assertThat( tagId ).isEqualTo( "ConfiguredTag-Test" ); } @Test diff --git a/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/StepsAreReportedTest.java b/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/StepsAreReportedTest.java index 997b52a5fc..2bce6a66e0 100644 --- a/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/StepsAreReportedTest.java +++ b/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/StepsAreReportedTest.java @@ -1,6 +1,6 @@ package com.tngtech.jgiven.junit; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -17,11 +17,7 @@ import com.tngtech.jgiven.annotation.IsTag; import com.tngtech.jgiven.annotation.NotImplementedYet; import com.tngtech.jgiven.junit.StepsAreReportedTest.TestSteps; -import com.tngtech.jgiven.report.model.ExecutionStatus; -import com.tngtech.jgiven.report.model.ScenarioCaseModel; -import com.tngtech.jgiven.report.model.ScenarioModel; -import com.tngtech.jgiven.report.model.StepModel; -import com.tngtech.jgiven.report.model.Word; +import com.tngtech.jgiven.report.model.*; @RunWith( DataProviderRunner.class ) public class StepsAreReportedTest extends ScenarioTest { @@ -34,19 +30,19 @@ public void given_steps_are_reported() throws Throwable { getScenario().finished(); ScenarioModel model = getScenario().getModel().getLastScenarioModel(); - assertThat(model.getClassName()).isEqualTo( StepsAreReportedTest.class.getName() ); - assertThat(model.getTestMethodName()).isEqualTo( "given_steps_are_reported" ); - assertThat(model.getDescription()).isEqualTo( "given steps are reported" ); + assertThat( model.getClassName() ).isEqualTo( StepsAreReportedTest.class.getName() ); + assertThat( model.getTestMethodName() ).isEqualTo( "given_steps_are_reported" ); + assertThat( model.getDescription() ).isEqualTo( "given steps are reported" ); assertThat( model.getExplicitParameters() ).isEmpty(); - assertThat(model.getTags()).isEmpty(); + assertThat( model.getTagIds() ).isEmpty(); assertThat( model.getScenarioCases() ).hasSize( 1 ); ScenarioCaseModel scenarioCase = model.getCase( 0 ); assertThat( scenarioCase.getExplicitArguments() ).isEmpty(); - assertThat(scenarioCase.getCaseNr()).isEqualTo( 1 ); - assertThat(scenarioCase.getSteps()).hasSize( 1 ); + assertThat( scenarioCase.getCaseNr() ).isEqualTo( 1 ); + assertThat( scenarioCase.getSteps() ).hasSize( 1 ); - StepModel step = scenarioCase.getSteps().get(0); + StepModel step = scenarioCase.getSteps().get( 0 ); assertThat( step.name ).isEqualTo( "some test step" ); assertThat( step.words ).isEqualTo( Arrays.asList( Word.introWord( "Given" ), new Word( "some test step" ) ) ); assertThat( step.isNotImplementedYet() ).isFalse(); @@ -60,7 +56,7 @@ public void steps_annotated_with_NotImplementedYet_are_recognized() throws Throw getScenario().finished(); ScenarioModel model = getScenario().getModel().getLastScenarioModel(); - StepModel stepModel = model.getCase(0).getSteps().get(0); + StepModel stepModel = model.getCase( 0 ).getSteps().get( 0 ); assertThat( stepModel.isNotImplementedYet() ).isTrue(); assertThat( model.getExecutionStatus() ).isEqualTo( ExecutionStatus.NONE_IMPLEMENTED ); } @@ -88,11 +84,17 @@ public void annotations_are_translated_to_tags() throws Throwable { given().some_test_step(); getScenario().finished(); - ScenarioModel model = getScenario().getModel().getLastScenarioModel(); - assertThat(model.getTags()).hasSize( 1 ); + ReportModel reportModel = getScenario().getModel(); + ScenarioModel model = reportModel.getLastScenarioModel(); + assertThat( model.getTagIds() ).hasSize( 1 ); + + String tagId = model.getTagIds().get( 0 ); + assertThat( tagId ).isEqualTo( "TestTag-foo, bar, baz" ); - assertThat( model.getTags().get( 0 ).getName() ).isEqualTo( "TestTag" ); - assertThat( model.getTags().get( 0 ).getValues() ).containsExactly( "foo", "bar", "baz" ); + Tag tag = reportModel.getTagWithId( tagId ); + assertThat( tag ).isNotNull(); + assertThat( tag.getName() ).isEqualTo( "TestTag" ); + assertThat( tag.getValues() ).containsExactly( "foo", "bar", "baz" ); } @DataProvider @@ -107,11 +109,16 @@ public void annotations_are_translated_to_tags_only_once( int n ) throws Throwab given().some_test_step(); getScenario().finished(); + ReportModel reportModel = getScenario().getModel(); ScenarioModel model = getScenario().getModel().getLastScenarioModel(); - assertThat(model.getTags()).hasSize( 1 ); + assertThat( model.getTagIds() ).hasSize( 1 ); + + String tagId = model.getTagIds().get( 0 ); - assertThat( model.getTags().get( 0 ).getName() ).isEqualTo( "TestTag" ); - assertThat( model.getTags().get( 0 ).getValues() ).containsExactly( "foo", "bar", "baz" ); + Tag tag = reportModel.getTagWithId( tagId ); + assertThat( tag ).isNotNull(); + assertThat( tag.getName() ).isEqualTo( "TestTag" ); + assertThat( tag.getValues() ).containsExactly( "foo", "bar", "baz" ); } @Test diff --git a/jgiven-tests/build.gradle b/jgiven-tests/build.gradle index 41f93733a0..1bd97b4bd3 100644 --- a/jgiven-tests/build.gradle +++ b/jgiven-tests/build.gradle @@ -12,6 +12,7 @@ dependencies { testCompile group: 'com.tngtech.java', name: 'junit-dataprovider', version: junitDataproviderVersion testCompile group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '2.43.0' testCompile group: 'com.github.detro.ghostdriver', name: 'phantomjsdriver', version: '1.1.0' + testCompile 'org.apache.commons:commons-io:1.3.2' } test.finalizedBy(jgivenReport) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java index 509e07d224..d74d1992de 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java @@ -5,6 +5,7 @@ import com.tngtech.jgiven.annotation.IsTag; +@FeatureReport @IsTag( type = "Feature", value = "HTML5 Report", description = "In order to have an interactive JGiven report for non-developers
    " + "As a developer,
    " diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java index 07b6744960..e1f52d5e7c 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java @@ -5,6 +5,7 @@ import com.tngtech.jgiven.annotation.IsTag; +@FeatureReport @IsTag( type = "Feature", value = "Static HTML Report", description = "In order to show JGiven scenarios to non-developers
    " + "As a developer,
    " diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java new file mode 100644 index 0000000000..896911326a --- /dev/null +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java @@ -0,0 +1,12 @@ +package com.tngtech.jgiven.tags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.tngtech.jgiven.annotation.IsTag; + +@IsTag( type = "Feature", value = "Report" ) +@Retention( RetentionPolicy.RUNTIME ) +public @interface FeatureReport { + +} diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java index 61e360d7b9..73107da609 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java @@ -5,6 +5,7 @@ import com.tngtech.jgiven.annotation.IsTag; +@FeatureReport @IsTag( type = "Feature", value = "Plain Text Report", description = "Plain text reports can be generated" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureTextReport { diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/ThenReportGenerator.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/ThenReportGenerator.java index 655cd7aa2f..3d4846f319 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/ThenReportGenerator.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/ThenReportGenerator.java @@ -11,6 +11,7 @@ import com.google.common.io.Files; import com.tngtech.jgiven.Stage; import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.annotation.Quoted; import com.tngtech.jgiven.report.model.ReportModel; public class ThenReportGenerator> extends Stage { @@ -21,22 +22,27 @@ public class ThenReportGenerator> extends St @ExpectedScenarioState protected List reportModels; - public SELF a_file_with_name_$_exists( String name ) { - assertThat( new File( targetReportDir, name ) ).exists(); + public SELF a_file_with_name_$_exists(@Quoted String name) { + assertThat(new File(targetReportDir, name)).exists(); return self(); } - public SELF file_$_contains_pattern( String fileName, final String regexp ) throws IOException { - String content = Files.asCharSource( new File( targetReportDir, fileName ), Charset.forName( "utf8" ) ).read(); - Pattern pattern = Pattern.compile( ".*" + regexp + ".*", Pattern.MULTILINE | Pattern.DOTALL ); + public SELF a_file_$_exists_in_folder_$(@Quoted String name, @Quoted String folder) { + assertThat(new File(new File(targetReportDir, folder), name)).exists(); + return self(); + } + + public SELF file_$_contains_pattern(@Quoted String fileName, @Quoted final String regexp) throws IOException { + String content = Files.asCharSource(new File(targetReportDir, fileName), Charset.forName("utf8")).read(); + Pattern pattern = Pattern.compile(".*" + regexp + ".*", Pattern.MULTILINE | Pattern.DOTALL); - assertThat( pattern.matcher( regexp ).matches() ).as( "file " + fileName + " does not contain " + regexp ).isTrue(); + assertThat(pattern.matcher(regexp).matches()).as("file " + fileName + " does not contain " + regexp).isTrue(); return self(); } - public SELF file_$_contains( String fileName, final String string ) throws IOException { - String content = Files.asCharSource( new File( targetReportDir, fileName ), Charset.forName( "utf8" ) ).read(); - assertThat( content ).as( "file " + fileName + " does not contain " + string ).contains( string ); + public SELF file_$_contains(@Quoted String fileName, @Quoted final String string) throws IOException { + String content = Files.asCharSource(new File(targetReportDir, fileName), Charset.forName("utf8")).read(); + assertThat(content).as("file " + fileName + " does not contain " + string).contains(string); return self(); } } diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/WhenReportGenerator.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/WhenReportGenerator.java index a49e619eb8..aa3e9f087d 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/WhenReportGenerator.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/WhenReportGenerator.java @@ -42,7 +42,7 @@ public void the_asciidoc_reporter_is_executed() throws IOException { new AsciiDocReportGenerator().generate( getCompleteReportModel(), targetReportDir ); } - private CompleteReportModel getCompleteReportModel() { + protected CompleteReportModel getCompleteReportModel() { return new ReportModelReader().readDirectory( jsonReportDirectory ); } diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5ReportStage.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5AppStage.java similarity index 90% rename from jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5ReportStage.java rename to jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5AppStage.java index 96bea2912f..4d206da71e 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5ReportStage.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5AppStage.java @@ -12,7 +12,7 @@ import com.tngtech.jgiven.attachment.Attachment; import com.tngtech.jgiven.attachment.MediaType; -public class Html5ReportStage> extends Stage { +public class Html5AppStage> extends Stage { @ExpectedScenarioState protected CurrentStep currentStep; diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5GeneratorTest.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5AppTest.java similarity index 94% rename from jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5GeneratorTest.java rename to jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5AppTest.java index f8b665da6a..b5eb5a7bca 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5GeneratorTest.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5AppTest.java @@ -11,6 +11,7 @@ import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.jgiven.JGivenScenarioTest; +import com.tngtech.jgiven.annotation.Description; import com.tngtech.jgiven.annotation.ProvidedScenarioState; import com.tngtech.jgiven.annotation.ScenarioStage; import com.tngtech.jgiven.report.WhenReportGenerator; @@ -21,8 +22,9 @@ import com.tngtech.jgiven.tags.Issue; @FeatureHtml5Report +@Description( "Tests against the generated HTML5 App using WebDriver" ) @RunWith( DataProviderRunner.class ) -public class Html5GeneratorTest extends JGivenScenarioTest, WhenHtml5Report, ThenHtml5Report> { +public class Html5AppTest extends JGivenScenarioTest, WhenHtml5App, ThenHtml5App> { @ScenarioStage private WhenReportGenerator whenReport; diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5ReportGeneratorTest.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5ReportGeneratorTest.java new file mode 100644 index 0000000000..4acdde6cd7 --- /dev/null +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5ReportGeneratorTest.java @@ -0,0 +1,33 @@ +package com.tngtech.jgiven.report.html5; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.jgiven.JGivenScenarioTest; +import com.tngtech.jgiven.annotation.Description; +import com.tngtech.jgiven.report.json.GivenJsonReports; +import com.tngtech.jgiven.tags.FeatureHtml5Report; +import com.tngtech.jgiven.tags.FeatureTags; + +@FeatureHtml5Report +@Description( "Tests against the generated HTML5 App using WebDriver" ) +@RunWith( DataProviderRunner.class ) +public class Html5ReportGeneratorTest extends + JGivenScenarioTest, WhenHtml5ReportGenerator, ThenHtml5ReportGenerator> { + + @Test + @FeatureTags + @Description( "the HTML5 report generator creates a 'tags.js' file" ) + public void the_HTML5_report_generator_creates_a_tags_file() throws Exception { + given().a_report_model() + .and().scenario_$_has_tag_$_with_value_$( 1, "TestTag", "123" ) + .and().the_report_exist_as_JSON_file(); + + when().the_HTML5_Report_Generator_is_executed(); + + then().a_file_$_exists_in_folder_$( "tags.js", "data" ) + .and().a_file_$_exists_in_folder_$( "metaData.js", "data" ); + } + +} diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5Report.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5App.java similarity index 93% rename from jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5Report.java rename to jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5App.java index 4acf928ac0..39b417e9c8 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5Report.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5App.java @@ -12,7 +12,7 @@ import org.openqa.selenium.WebElement; import org.testng.reporters.Files; -public class ThenHtml5Report> extends Html5ReportStage { +public class ThenHtml5App> extends Html5AppStage { public SELF the_page_title_is( String pageTitle ) { assertThat( webDriver.findElement( By.id( "page-title" ) ).getText() ).isEqualTo( pageTitle ); diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5ReportGenerator.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5ReportGenerator.java new file mode 100644 index 0000000000..f53893279b --- /dev/null +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/ThenHtml5ReportGenerator.java @@ -0,0 +1,5 @@ +package com.tngtech.jgiven.report.html5; + +import com.tngtech.jgiven.report.ThenReportGenerator; + +public class ThenHtml5ReportGenerator> extends ThenReportGenerator {} diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5Report.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5App.java similarity index 96% rename from jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5Report.java rename to jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5App.java index 09be6ff475..724aa2627b 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5Report.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5App.java @@ -13,7 +13,7 @@ import com.tngtech.jgiven.report.model.ReportModel; import com.tngtech.jgiven.report.model.ScenarioModel; -public class WhenHtml5Report> extends Html5ReportStage { +public class WhenHtml5App> extends Html5AppStage { @ExpectedScenarioState protected List reportModels; diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5ReportGenerator.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5ReportGenerator.java new file mode 100644 index 0000000000..82cf2d84fb --- /dev/null +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/WhenHtml5ReportGenerator.java @@ -0,0 +1,28 @@ +package com.tngtech.jgiven.report.html5; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; + +import com.tngtech.jgiven.report.WhenReportGenerator; + +public class WhenHtml5ReportGenerator> extends WhenReportGenerator { + + public SELF the_HTML5_Report_Generator_is_executed() { + Html5ReportGenerator html5ReportGenerator = new Html5ReportGenerator() { + @Override + protected void unzipApp( File toDir ) throws IOException { + try { + super.unzipApp( toDir ); + } catch( Exception e ) { + // unzipping does not work when testing within the IDE + FileUtils.copyDirectory( new File( "jgiven-html5-report/src/app" ), toDir ); + } + } + }; + + html5ReportGenerator.generate( getCompleteReportModel(), targetReportDir ); + return self(); + } +} diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModel.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModel.java index 21f22021ea..4f1ad96dc7 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModel.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModel.java @@ -199,6 +199,7 @@ public SELF the_first_scenario_has_tag( @Quoted String name ) { public SELF scenario_$_has_tag_$_with_value_$( int i, String name, String value ) { latestTag = new Tag( name, value ).setPrependType( true ); reportModel.getScenarios().get( i - 1 ).addTag( latestTag ); + reportModel.addTag( latestTag ); return self(); } diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/ThenReportModel.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/ThenReportModel.java index efa4723151..fab29e44c2 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/ThenReportModel.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/ThenReportModel.java @@ -46,9 +46,9 @@ public void an_error_message_is_stored_in_the_report() { } public void the_report_model_contains_a_tag_named( String tagName ) { - List tags = reportModel.getLastScenarioModel().getTags(); + List tags = reportModel.getLastScenarioModel().getTagIds(); assertThat( tags ).isNotEmpty(); - assertThat( tags ).extracting( "name" ).contains( tagName ); + assertThat( tags ).contains( tagName + "-testValue" ); } public void the_description_of_the_report_model_is( String description ) { From 13cfc03a6e37ee3f0a7355b5038b000682faa0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sat, 1 Aug 2015 14:23:49 +0200 Subject: [PATCH 02/13] HTML5 Report: added karma test --- jgiven-html5-report/bower.json | 3 +- jgiven-html5-report/karma.conf.js | 76 +++++++++++++++++++ jgiven-html5-report/package.json | 9 ++- jgiven-html5-report/src/app/index.html | 2 +- .../src/app/lib/classService.js | 26 +++++-- .../src/app/lib/dataService.js | 20 +---- jgiven-html5-report/src/app/lib/reportCtrl.js | 12 ++- jgiven-html5-report/src/app/lib/tagService.js | 17 +++-- .../src/test/app/tagServiceTest.js | 64 ++++++++++++++++ 9 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 jgiven-html5-report/karma.conf.js create mode 100644 jgiven-html5-report/src/test/app/tagServiceTest.js diff --git a/jgiven-html5-report/bower.json b/jgiven-html5-report/bower.json index 37d824e586..24b9b8f8fd 100644 --- a/jgiven-html5-report/bower.json +++ b/jgiven-html5-report/bower.json @@ -29,6 +29,7 @@ "lodash": "2.4.1", "foundation": "5.5.2", "angular-chart.js": "0.7.2", - "angular-local-storage": "0.1.5" + "angular-local-storage": "0.1.5", + "angular-mocks": "1.4.1" } } diff --git a/jgiven-html5-report/karma.conf.js b/jgiven-html5-report/karma.conf.js new file mode 100644 index 0000000000..9c1b9db6c9 --- /dev/null +++ b/jgiven-html5-report/karma.conf.js @@ -0,0 +1,76 @@ +// Karma configuration +// Generated on Sat Aug 01 2015 13:13:27 GMT+0200 (CEST) + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jasmine'], + + + // list of files / patterns to load in the browser + files: [ + 'build/bower_components/**/dist/jquery.js', + 'build/bower_components/**/angular.js', + 'build/bower_components/**/angular-sanitize.js', + 'build/bower_components/**/angular-mocks.js', + 'build/bower_components/**/mm-foundation-tpls.js', + 'build/bower_components/**/foundation.js', + 'build/bower_components/**/Chart.js', + 'build/bower_components/**/angular-chart.js', + 'build/bower_components/**/angular-local-storage.js', + 'build/bower_components/**/lodash.js', + 'src/app/**/*.js', + 'src/test/app/**/*.js' + ], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false + }) +} diff --git a/jgiven-html5-report/package.json b/jgiven-html5-report/package.json index 501266782c..e0518f790f 100644 --- a/jgiven-html5-report/package.json +++ b/jgiven-html5-report/package.json @@ -3,8 +3,15 @@ "version": "0.7.4", "description": "An HTML5 report for JGiven", "main": "src/app/app.js", + "scripts": { + "test": "./node_modules/karma/bin/karma start --debug" + }, "devDependencies": { - "bower": "~1.3.12" + "bower": "~1.3.12", + "karma": "~0.13.3", + "jasmine-core": "~2.3.4", + "karma-jasmine": "~0.3.6", + "karma-chrome-launcher": "~0.2.0" }, "keywords": [ "jgiven", diff --git a/jgiven-html5-report/src/app/index.html b/jgiven-html5-report/src/app/index.html index 9a83e13dc8..bb0459ad9c 100644 --- a/jgiven-html5-report/src/app/index.html +++ b/jgiven-html5-report/src/app/index.html @@ -133,7 +133,7 @@

    JGiven Report

  • Back
  • - {{tag.prependType ? + {{tag.prependType ? tag.name + '-' : ''}}{{tag.value}}
  • diff --git a/jgiven-html5-report/src/app/lib/classService.js b/jgiven-html5-report/src/app/lib/classService.js index 1aea428c36..60813a2cdd 100644 --- a/jgiven-html5-report/src/app/lib/classService.js +++ b/jgiven-html5-report/src/app/lib/classService.js @@ -5,30 +5,38 @@ jgivenReportApp.factory('classService', ['dataService', function (dataService) { 'use strict'; + /** + * Maps full qualified class names to lists of scenarios + */ var classNameScenarioMap = getClassNameScenarioMap(); + + /** + * Maps full qualified package names to package node objects + */ var packageNodeMap = {}; + var rootPackage = getRootPackage(); function getClassNameScenarioMap() { var classNameScenarioMap = {}; - var testClasses = dataService.getTestClasses(); + var testCases = dataService.getTestCases(); - _.forEach(testClasses, function (testClass) { - classNameScenarioMap[testClass.className] = testClass; + _.forEach(testCases, function (testCase) { + classNameScenarioMap[testCase.className] = testCase; }); return classNameScenarioMap; } - function getTestClasses() { - return dataService.getTestClasses(); + function getTestCases() { + return dataService.getTestCases(); } /** * Builds up the navigation tree for classes */ function getRootPackage() { - var allClasses = getTestClasses(); + var allClasses = getTestCases(); var rootPackage = getPackageNode(""); _.forEach(allClasses, function (testClass) { @@ -130,9 +138,13 @@ jgivenReportApp.factory('classService', ['dataService', function (dataService) { }); } + function getTestCaseByClassName(className) { + return classNameScenarioMap[className]; + } return { - getTestClasses: getTestClasses, + getTestCases: getTestCases, + getTestCaseByClassName: getTestCaseByClassName, getScenariosOfPackage: getScenariosOfPackage, getRootPackage: function () { return rootPackage; diff --git a/jgiven-html5-report/src/app/lib/dataService.js b/jgiven-html5-report/src/app/lib/dataService.js index d7a8ea1465..df6057116d 100644 --- a/jgiven-html5-report/src/app/lib/dataService.js +++ b/jgiven-html5-report/src/app/lib/dataService.js @@ -6,10 +6,10 @@ jgivenReportApp.factory('dataService', [function () { 'use strict'; var tagFile = jgivenReport.tagFile; - var scenarios = jgivenReport.scenarios; + var testCases = jgivenReport.scenarios; function getAllScenarios() { - return _.flatten(_.map(scenarios, function (x) { + return _.flatten(_.map(testCases, function (x) { return x.scenarios; }), true); } @@ -30,26 +30,14 @@ jgivenReportApp.factory('dataService', [function () { }); } - function getTagByTagId(tagId) { - var tagInstance = tagFile.tags[tagId]; - var tagType = tagFile.tagTypeMap[tagInstance.tagType]; - var tag = Object.create(tagType); - tag.value = tagInstance.value; - return tag; - } - return { getTagFile: function () { return tagFile; }, - getScenarios: function () { - return scenarios; - }, - - getTestClasses: function () { - return scenarios; + getTestCases: function () { + return testCases; }, getAllScenarios: getAllScenarios, diff --git a/jgiven-html5-report/src/app/lib/reportCtrl.js b/jgiven-html5-report/src/app/lib/reportCtrl.js index 78724657b2..cfcc75e463 100644 --- a/jgiven-html5-report/src/app/lib/reportCtrl.js +++ b/jgiven-html5-report/src/app/lib/reportCtrl.js @@ -147,7 +147,7 @@ jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $ti }; $scope.updateCurrentPageToClassName = function (className, options) { - $scope.updateCurrentPageToTestCase($scope.classNameScenarioMap[className], options); + $scope.updateCurrentPageToTestCase(classService.getTestCaseByClassName(className), options); }; $scope.updateCurrentPageToPackage = function (packageName, options) { @@ -209,7 +209,7 @@ jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $ti }; $scope.showScenario = function (className, methodName, options) { - var scenarios = sortByDescription(_.filter($scope.classNameScenarioMap[className].scenarios, function (x) { + var scenarios = sortByDescription(_.filter(classService.getTestCaseByClassName(className).scenarios, function (x) { return x.testMethodName === methodName; })); $scope.currentPage = { @@ -466,9 +466,15 @@ jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $ti $scope.getUrlFromTagId = function getUrlFromTagId(tagId) { var tag = $scope.getTagByTagId(tagId); - return '#tag/' + getTagName(tag) + '/' + $window.encodeURIComponent(tag.value); + return $scope.getUrlFromTag(tag); }; + $scope.getUrlFromTag = function getUrlFromTag(tag) { + return '#tag/' + getTagName(tag) + + (tag.value ? '/' + $window.encodeURIComponent(tag.value) : ''); + + } + $scope.getTagByTagId = function (tagId) { return tagService.getTagByTagId(tagId); }; diff --git a/jgiven-html5-report/src/app/lib/tagService.js b/jgiven-html5-report/src/app/lib/tagService.js index 83a62b74f1..3f585b628e 100644 --- a/jgiven-html5-report/src/app/lib/tagService.js +++ b/jgiven-html5-report/src/app/lib/tagService.js @@ -8,7 +8,7 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { /** * Maps tag IDs to tags and their scenarios */ - var tagScenarioMap = getTagScenarioMap(dataService.getScenarios()); + var tagScenarioMap = getTagScenarioMap(dataService.getTestCases()); /** * Maps tag keys to tag nodes @@ -123,7 +123,8 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { var node = createNode(tagToString(tag)); node.url = function () { - return '#tag/' + window.encodeURIComponent(getTagName(tag)) + '/' + window.encodeURIComponent(tag.value); + return '#tag/' + window.encodeURIComponent(getTagName(tag)) + + (tag.value ? '/' + window.encodeURIComponent(tag.value) : ''); }; node.scenarios = function () { @@ -199,10 +200,6 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { return tagScenarioMap[getTagKey(tag)].scenarios; } - function getTagByTagId(tagId) { - return dataService.getTagByTagId(tagId); - } - function getTagByKey(tagKey) { return tagScenarioMap[tagKey].tag; } @@ -211,6 +208,14 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { return tagNameMap[name]; } + function getTagByTagId(tagId) { + var tagInstance = dataService.getTagFile().tags[tagId]; + var tagType = dataService.getTagFile().tagTypeMap[tagInstance.tagType]; + var tag = Object.create(tagType); + tag.value = tagInstance.value; + return tag; + } + return { getScenariosByTag: getScenariosByTag, getTagByTagId: getTagByTagId, diff --git a/jgiven-html5-report/src/test/app/tagServiceTest.js b/jgiven-html5-report/src/test/app/tagServiceTest.js new file mode 100644 index 0000000000..d5d5c0533c --- /dev/null +++ b/jgiven-html5-report/src/test/app/tagServiceTest.js @@ -0,0 +1,64 @@ +describe("TagService", function () { + beforeEach(module('jgivenReportApp')); + + beforeEach(function () { + var tagFile = { + tagTypeMap: { + 'testtag': { + 'type': 'testtag', + 'tags': ['categoryA'] + }, + 'categoryA': { + 'type': 'categoryA' + } + }, + tags: { + 'testtag-1': { + tagType: 'testtag', + value: '1' + }, + 'testtag-2': { + tagType: 'testtag', + value: '2' + }, + 'categoryA': { + tagType: 'categoryA' + } + } + }; + + var testCases = [{ + scenarios: [{ + tagIds: ['testtag-1', 'testtag-2'] + }] + }]; + + function createDataServiceMock(tagFile, testCases) { + return { + getTagFile: function () { + return tagFile; + } + , + getTestCases: function () { + return testCases; + } + } + } + + module(function ($provide) { + $provide.value('dataService', createDataServiceMock(tagFile, testCases)); + }); + + + }); + + beforeEach(inject(function (_tagService_) { + tagService = _tagService_; + })); + + it("calculates root tags correctly", function () { + var rootTags = tagService.getRootTags(); + expect(rootTags).toBeDefined(); + expect(rootTags.length).toBe(2); + }); +}); \ No newline at end of file From 4cf7f4f075dd86b9cb17f43d47a55250f826232c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sat, 1 Aug 2015 18:32:39 +0200 Subject: [PATCH 03/13] HTML5 Report: improved hierarchical tags --- jgiven-html5-report/package.json | 3 +- jgiven-html5-report/src/app/lib/tagService.js | 41 ++++++---- jgiven-html5-report/src/app/lib/util.js | 4 + .../src/test/app/tagServiceTest.js | 82 ++++++++++++++++--- .../java/com/tngtech/jgiven/tags/Feature.java | 15 ++++ .../com/tngtech/jgiven/tags/FeatureCore.java | 12 +++ 6 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java create mode 100644 jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java diff --git a/jgiven-html5-report/package.json b/jgiven-html5-report/package.json index e0518f790f..fcaa31a1fe 100644 --- a/jgiven-html5-report/package.json +++ b/jgiven-html5-report/package.json @@ -4,7 +4,8 @@ "description": "An HTML5 report for JGiven", "main": "src/app/app.js", "scripts": { - "test": "./node_modules/karma/bin/karma start --debug" + "test": "./node_modules/karma/bin/karma start --debug --single-run", + "test-watch": "./node_modules/karma/bin/karma start --debug" }, "devDependencies": { "bower": "~1.3.12", diff --git a/jgiven-html5-report/src/app/lib/tagService.js b/jgiven-html5-report/src/app/lib/tagService.js index 3f585b628e..9bffab088a 100644 --- a/jgiven-html5-report/src/app/lib/tagService.js +++ b/jgiven-html5-report/src/app/lib/tagService.js @@ -71,32 +71,33 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { * navigation and returns the list of root nodes */ function getRootTags() { - var rootTags; - _.forEach(_.values(tagScenarioMap), function (tagEntry) { var tagNode = getTagNode(tagEntry); - var name = getTagName(tagEntry.tag); - var nameNode = tagNameMap[name]; - if (!nameNode) { - nameNode = createNameNode(name); - tagNameMap[name] = nameNode; + var name = tagEntry.tag.name; + if (name) { + var nameNode = tagNameMap[name]; + if (!nameNode) { + nameNode = createNameNode(name); + tagNameMap[name] = nameNode; + } + nameNode.addTagNode(tagNode); } - nameNode.addTagNode(tagNode); }); - rootTags = _.filter(_.values(tagNameMap), function (tagNode) { - return tagNode.subTags().length > 1; + var nameNodesWithMultipleEntries = _.filter(_.values(tagNameMap), function (nameNode) { + return nameNode.subTags().length > 1; + }); + + var nodesWithoutParents = _.filter(_.values(tagNodeMap), function (tagNode) { + return undefinedOrEmpty(tagNode.tag().tags); }); - rootTags = rootTags.concat(_.map(_.filter(_.values(tagNameMap), function (tagNode) { - return tagNode.subTags().length === 1 - && (!tagNode.subTags()[0].tag().tags - || tagNode.subTags()[0].tag().tags.length === 0); - }), function (tagNode) { - return tagNode.subTags()[0]; - })); - return rootTags; + return _.sortBy(nameNodesWithMultipleEntries.concat(nodesWithoutParents), + function (tagNode) { + return tagNode.nodeName(); + }); + function getTagNode(tagEntry) { var tag = tagEntry.tag; @@ -139,6 +140,10 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { return node; } + /** + * A name node is a pseudo tag node that + * has as sub nodes all tags with the same name + */ function createNameNode(name) { var node = createNode(name); diff --git a/jgiven-html5-report/src/app/lib/util.js b/jgiven-html5-report/src/app/lib/util.js index c0fccc1b9e..43917df478 100644 --- a/jgiven-html5-report/src/app/lib/util.js +++ b/jgiven-html5-report/src/app/lib/util.js @@ -2,6 +2,10 @@ * Utility functions */ +function undefinedOrEmpty(array) { + return !array || array.length === 0; +} + function getTagName(tag) { return tag.name ? tag.name : tag.type; } diff --git a/jgiven-html5-report/src/test/app/tagServiceTest.js b/jgiven-html5-report/src/test/app/tagServiceTest.js index d5d5c0533c..87cc8e21bc 100644 --- a/jgiven-html5-report/src/test/app/tagServiceTest.js +++ b/jgiven-html5-report/src/test/app/tagServiceTest.js @@ -4,35 +4,90 @@ describe("TagService", function () { beforeEach(function () { var tagFile = { tagTypeMap: { - 'testtag': { - 'type': 'testtag', - 'tags': ['categoryA'] + 'issue': { + 'type': 'issue', + 'tags': ['categoryA1'] }, 'categoryA': { 'type': 'categoryA' + }, + 'categoryB': { + 'type': 'categoryB' + }, + 'categoryA1': { + 'type': 'categoryA1', + 'tags': ['categoryA'] + }, + 'featureA': { + 'type': 'featureA', + 'name': 'feature', + 'tags': ['categoryA'] + }, + 'featureB': { + 'type': 'featureB', + 'name': 'feature', + 'tags': ['categoryB'] + }, + 'somethingA': { + 'type': 'somethingA', + 'name': 'something' } }, tags: { - 'testtag-1': { - tagType: 'testtag', - value: '1' + 'issue-1': { + 'tagType': 'issue', + 'value': '1' }, - 'testtag-2': { - tagType: 'testtag', - value: '2' + 'issue-2': { + 'tagType': 'issue', + 'value': '2' }, 'categoryA': { - tagType: 'categoryA' + 'tagType': 'categoryA' + }, + 'categoryA1': { + 'tagType': 'categoryA1' + }, + 'categoryB': { + 'tagType': 'categoryB' + }, + 'featureA': { + 'tagType': 'featureA', + 'value': 'A' + }, + 'featureB': { + 'tagType': 'featureB', + 'value': 'B' + }, + 'somethingA': { + 'tagType': 'somethingA', + 'value': 'someA' } + } }; var testCases = [{ scenarios: [{ - tagIds: ['testtag-1', 'testtag-2'] + tagIds: ['issue-1', 'issue-2', 'categoryB', 'featureA', 'featureB', 'somethingA'] }] }]; + /** Expected Tag Tree: + + categoryA + --+ categoryA1 + ----+ issue + ------+ issue-1 + ------+ issue-2 + --+ featureA + + categoryB + --+ featureB + + feature + --+ featureA + --+ featureB + */ + + function createDataServiceMock(tagFile, testCases) { return { getTagFile: function () { @@ -59,6 +114,9 @@ describe("TagService", function () { it("calculates root tags correctly", function () { var rootTags = tagService.getRootTags(); expect(rootTags).toBeDefined(); - expect(rootTags.length).toBe(2); + var rootTagNames = _.map(rootTags, function (rootTag) { + return rootTag.nodeName(); + }); + expect(rootTagNames).toEqual(['categoryA', 'categoryB', 'feature', 'someA']); }); }); \ No newline at end of file diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java new file mode 100644 index 0000000000..481cfcfd0d --- /dev/null +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java @@ -0,0 +1,15 @@ +package com.tngtech.jgiven.tags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.tngtech.jgiven.annotation.IsTag; + +@IsTag( type = "Feature", value = "Case Diffs", + description = "In order to get a better overview over structurally different cases of a scenario
    " + + "As a human,
    " + + "I want the differences highlighted in the generated report" ) +@Retention( RetentionPolicy.RUNTIME ) +public @interface Feature { + +} diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java new file mode 100644 index 0000000000..b667b66d01 --- /dev/null +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java @@ -0,0 +1,12 @@ +package com.tngtech.jgiven.tags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.tngtech.jgiven.annotation.IsTag; + +@IsTag( type = "Feature", value = "Report" ) +@Retention( RetentionPolicy.RUNTIME ) +public @interface FeatureCore { + +} From f5adf1a8fbb8002d6aaccfaa531aff5da5690f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sat, 1 Aug 2015 18:33:12 +0200 Subject: [PATCH 04/13] use hierarchical tags in JGivens own tests --- .../src/main/java/com/tngtech/jgiven/tags/Feature.java | 5 +---- .../java/com/tngtech/jgiven/tags/FeatureAsciiDocReport.java | 3 ++- .../java/com/tngtech/jgiven/tags/FeatureAttachments.java | 3 ++- .../main/java/com/tngtech/jgiven/tags/FeatureCaseDiffs.java | 3 ++- .../src/main/java/com/tngtech/jgiven/tags/FeatureCore.java | 3 ++- .../main/java/com/tngtech/jgiven/tags/FeatureDataTables.java | 3 ++- .../com/tngtech/jgiven/tags/FeatureDerivedParameters.java | 3 ++- .../main/java/com/tngtech/jgiven/tags/FeatureDuration.java | 3 ++- .../src/main/java/com/tngtech/jgiven/tags/FeatureGerman.java | 3 ++- .../java/com/tngtech/jgiven/tags/FeatureHtml5Report.java | 2 +- .../main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java | 2 +- .../src/main/java/com/tngtech/jgiven/tags/FeatureJUnit.java | 3 ++- .../com/tngtech/jgiven/tags/FeatureNotImplementedYet.java | 3 ++- .../src/main/java/com/tngtech/jgiven/tags/FeatureReport.java | 3 ++- .../java/com/tngtech/jgiven/tags/FeatureStepParameters.java | 3 ++- .../com/tngtech/jgiven/tags/FeatureTableStepArguments.java | 3 ++- .../src/main/java/com/tngtech/jgiven/tags/FeatureTestNg.java | 3 ++- .../main/java/com/tngtech/jgiven/tags/FeatureTextReport.java | 2 +- 18 files changed, 32 insertions(+), 21 deletions(-) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java index 481cfcfd0d..3b342168a4 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java @@ -5,10 +5,7 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Case Diffs", - description = "In order to get a better overview over structurally different cases of a scenario
    " - + "As a human,
    " - + "I want the differences highlighted in the generated report" ) +@IsTag @Retention( RetentionPolicy.RUNTIME ) public @interface Feature { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAsciiDocReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAsciiDocReport.java index 583f34fee0..e2af72f66f 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAsciiDocReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAsciiDocReport.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "AsciiDoc Report", +@FeatureReport +@IsTag( value = "AsciiDoc Report", description = "In order to easily combine hand-written documentation with JGiven scenarios
    " + "As a developer,
    " + "I want that JGiven generates AsciiDoc reports" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAttachments.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAttachments.java index c39aaec6e7..adb497efcf 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAttachments.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAttachments.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Attachments", +@FeatureCore +@IsTag( value = "Attachments", description = "In order to get additional information about a step, like screenshots, for example
    " + "As a JGiven user,
    " + "I want that steps can have attachments" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCaseDiffs.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCaseDiffs.java index 20f7857b0d..f4d265006c 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCaseDiffs.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCaseDiffs.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Case Diffs", +@FeatureHtml5Report +@IsTag( value = "Case Diffs", description = "In order to get a better overview over structurally different cases of a scenario
    " + "As a human,
    " + "I want the differences highlighted in the generated report" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java index b667b66d01..99528c5bdd 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Report" ) +@Feature +@IsTag( value = "Core Features" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureCore { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDataTables.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDataTables.java index a8c61f7902..8b5168ea08 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDataTables.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDataTables.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Data Tables", +@FeatureCore +@IsTag( value = "Data Tables", description = "In order to get a better overview over the different cases of a scenario
    " + "As a human,
    " + "I want to have different cases represented as a data table" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDerivedParameters.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDerivedParameters.java index b711e81d3e..a380603f73 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDerivedParameters.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDerivedParameters.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Derived Parameters", +@FeatureCore +@IsTag( value = "Derived Parameters", description = "In order to not have to specify easily derivable parameters explicitly
    " + "As a developer,
    " + "I want that step arguments derived from parameters appear in a data table" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDuration.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDuration.java index 4412556d35..3c0160e933 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDuration.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDuration.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Duration", description = "The duration of steps, cases, and scenarios is measured and reported" ) +@FeatureCore +@IsTag( value = "Duration", description = "The duration of steps, cases, and scenarios is measured and reported" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureDuration { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureGerman.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureGerman.java index 4af3c60006..3ad8e3902b 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureGerman.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureGerman.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "German Scenarios", description = "Scenarios can be written in German" ) +@FeatureCore +@IsTag( value = "German Scenarios", description = "Scenarios can be written in German" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureGerman { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java index d74d1992de..325a5463ac 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureReport -@IsTag( type = "Feature", value = "HTML5 Report", +@IsTag( value = "HTML5 Report", description = "In order to have an interactive JGiven report for non-developers
    " + "As a developer,
    " + "I want that JGiven generates HTML5 reports" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java index e1f52d5e7c..daf66fe7a4 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureReport -@IsTag( type = "Feature", value = "Static HTML Report", +@IsTag( value = "Static HTML Report", description = "In order to show JGiven scenarios to non-developers
    " + "As a developer,
    " + "I want that JGiven generates static HTML reports" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureJUnit.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureJUnit.java index 7716796284..c7eb45f87d 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureJUnit.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureJUnit.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "JUnit", +@Feature +@IsTag( value = "JUnit", description = "tests can be be executed with JUnit" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureJUnit { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureNotImplementedYet.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureNotImplementedYet.java index 7dd55229a8..c58d1e43d7 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureNotImplementedYet.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureNotImplementedYet.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "NotImplementedYet Annotation", +@FeatureCore +@IsTag( value = "NotImplementedYet Annotation", description = "As a good BDD practitioner,
    " + "I want to write my scenarios before I start coding
    " + "In order to discuss them with business stakeholders" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java index 896911326a..d4ed88009e 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Report" ) +@Feature +@IsTag( value = "Report Features" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureReport { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureStepParameters.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureStepParameters.java index 79a9bf0184..e4ce312ca1 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureStepParameters.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureStepParameters.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Step Parameters", description = "Steps can have parameters" ) +@FeatureCore +@IsTag( value = "Step Parameters", description = "Steps can have parameters" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureStepParameters { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTableStepArguments.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTableStepArguments.java index 2a346176ed..e7b96799b5 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTableStepArguments.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTableStepArguments.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Table Step Arguments", +@FeatureCore +@IsTag( value = "Table Step Arguments", description = "In order to better present table-like data
    " + "As a human,
    " + "I want a special treatment of table-like data in step arguments" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestNg.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestNg.java index 9c1e4bf995..43d6fbac41 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestNg.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestNg.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "TestNG", +@Feature +@IsTag( value = "TestNG", description = "tests can be be executed with TestNG" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureTestNg { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java index 73107da609..fc5fe84522 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureReport -@IsTag( type = "Feature", value = "Plain Text Report", description = "Plain text reports can be generated" ) +@IsTag( value = "Text Report", description = "Plain text reports can be generated" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureTextReport { From 73d1d7e8cba8cae284d305f742a14c6938450a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sat, 1 Aug 2015 21:43:15 +0200 Subject: [PATCH 05/13] HTML5 Report: further improvements of the hierarchical tag support --- CHANGELOG.md | 8 +- .../tngtech/jgiven/annotation/Pending.java | 68 ++++++ .../src/app/lib/dataService.js | 1 - jgiven-html5-report/src/app/lib/tagService.js | 24 ++- jgiven-html5-report/src/app/lib/util.js | 2 +- .../src/test/app/tagServiceTest.js | 198 +++++++++--------- .../java/com/tngtech/jgiven/tags/Feature.java | 2 +- .../jgiven/tags/FeatureAsciiDocReport.java | 2 +- .../jgiven/tags/FeatureAttachments.java | 2 +- .../tngtech/jgiven/tags/FeatureCaseDiffs.java | 4 +- .../com/tngtech/jgiven/tags/FeatureCore.java | 2 +- .../jgiven/tags/FeatureDataTables.java | 2 +- .../jgiven/tags/FeatureDerivedParameters.java | 2 +- .../tngtech/jgiven/tags/FeatureDuration.java | 2 +- .../tngtech/jgiven/tags/FeatureGerman.java | 2 +- .../jgiven/tags/FeatureHtml5Report.java | 2 +- .../jgiven/tags/FeatureHtmlReport.java | 2 +- .../com/tngtech/jgiven/tags/FeatureJUnit.java | 6 +- .../jgiven/tags/FeatureNotImplementedYet.java | 2 +- .../tngtech/jgiven/tags/FeatureReport.java | 2 +- .../jgiven/tags/FeatureStepParameters.java | 2 +- .../tags/FeatureTableStepArguments.java | 2 +- .../com/tngtech/jgiven/tags/FeatureTags.java | 3 +- .../jgiven/tags/FeatureTestFramework.java | 14 ++ .../tngtech/jgiven/tags/FeatureTestNg.java | 6 +- .../jgiven/tags/FeatureTextReport.java | 2 +- 26 files changed, 231 insertions(+), 133 deletions(-) create mode 100644 jgiven-core/src/main/java/com/tngtech/jgiven/annotation/Pending.java create mode 100644 jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestFramework.java diff --git a/CHANGELOG.md b/CHANGELOG.md index bc63574d05..626ef2f1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,17 @@ ## New Features +### Hierarchical Tags + +* Tags can now have parent tags by tagging a tag annotation. This allows you to define tag hierarchies. + ### Enhanced Spring Support [#94](https://github.com/TNG/JGiven/pull/94) * The Spring support has been greatly improved. JGiven Stages can now be directly managed by the Spring framework, resulting in a much better Spring integration. ** Note that the usage of Spring is optional and is provided by the `jgiven-spring` module. * Introduced `@JGivenStage` to ease writing spring beans that act as JGiven stage -### HTML5 Report +### Hierarchical Package Structure in the HTML5 Report * Classes are shown now in hierarchical navigation tree and scenarios can be listed by package [#91](https://github.com/TNG/JGiven/pull/91) @@ -17,7 +21,7 @@ * HTML5 Report: tables with duplicate entries cannot be used as step parameters [#89](https://github.com/TNG/JGiven/issues/89) * Fixed an issue that the `@Description` annotation was not regarded for methods with the `@IntroWord` [#87](https://github.com/TNG/JGiven/issues/87) -## New Annotation +## New Annotations * Introduced the `@As` annotation that replaces the `@Description` annotation when used on step methods and test methods. The `@Description` annotation should only be used for descriptions of test classes. diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/Pending.java b/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/Pending.java new file mode 100644 index 0000000000..1ea996c7c0 --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/Pending.java @@ -0,0 +1,68 @@ +package com.tngtech.jgiven.annotation; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Marks methods of step definitions as not implemented yet. + * Such steps will not be executed, but will appear in + * the report as not implemented yet. + *

    + * This is useful if one already wants to define the scenario without + * already implementing all steps, for example, to verify that + * all acceptance criteria of a story are covered by the scenario. + *

    + * Annotating a stage class indicates + * that no step is implemented yet. + *

    + * Finally, a test method can be annotated to indicate that the whole + * test is not implemented yet. The test will then be ignored by the testing-framework. + * (In fact an AssumptionException is thrown. It depends on the test runner how this + * is interpreted) + * Currently only works for JUnit + * + *

    Example

    + *
    + * {@literal @}NotImplementedYet
    + * public void my_cool_new_feature() {
    + *
    + * }
    + * 
    + * + */ +@Documented +@Inherited +@Retention( RUNTIME ) +@Target( { METHOD, TYPE } ) +@IsTag( ignoreValue = true, description = "Not implemented Scenarios" ) +public @interface Pending { + /** + * Optional description to describe when the implementation will be done. + */ + String value() default ""; + + /** + * Instead of only reporting not implemented yet steps, + * the steps are actually executed. + * This is useful to see whether some steps fail, for example. + * Failing steps, however, have no influence on the overall test result. + */ + boolean executeSteps() default false; + + /** + * If no step fails during the execution of the test, + * the test will fail. + *

    + * This makes sense if one ensures that a not implemented feature + * always leads to failing tests in the spirit of test-driven development. + *

    + * If this is true, the executeSteps attribute is implicitly true. + */ + boolean failIfPass() default false; +} diff --git a/jgiven-html5-report/src/app/lib/dataService.js b/jgiven-html5-report/src/app/lib/dataService.js index df6057116d..cd182f4c29 100644 --- a/jgiven-html5-report/src/app/lib/dataService.js +++ b/jgiven-html5-report/src/app/lib/dataService.js @@ -43,7 +43,6 @@ jgivenReportApp.factory('dataService', [function () { getAllScenarios: getAllScenarios, getPendingScenarios: getPendingScenarios, getFailedScenarios: getFailedScenarios, - getTagByTagId: getTagByTagId }; }]); diff --git a/jgiven-html5-report/src/app/lib/tagService.js b/jgiven-html5-report/src/app/lib/tagService.js index 9bffab088a..268ce51816 100644 --- a/jgiven-html5-report/src/app/lib/tagService.js +++ b/jgiven-html5-report/src/app/lib/tagService.js @@ -73,23 +73,27 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { function getRootTags() { _.forEach(_.values(tagScenarioMap), function (tagEntry) { var tagNode = getTagNode(tagEntry); - var name = tagEntry.tag.name; - if (name) { - var nameNode = tagNameMap[name]; - if (!nameNode) { - nameNode = createNameNode(name); - tagNameMap[name] = nameNode; - } - nameNode.addTagNode(tagNode); + var name = getTagName(tagEntry.tag); + var nameNode = tagNameMap[name]; + if (!nameNode) { + nameNode = createNameNode(name); + tagNameMap[name] = nameNode; } + nameNode.addTagNode(tagNode); }); var nameNodesWithMultipleEntries = _.filter(_.values(tagNameMap), function (nameNode) { return nameNode.subTags().length > 1; }); + _.forEach(nameNodesWithMultipleEntries, function (nameNode) { + _.forEach(nameNode.subTags(), function (subTag) { + subTag.nameNode = nameNode; + }); + }); + var nodesWithoutParents = _.filter(_.values(tagNodeMap), function (tagNode) { - return undefinedOrEmpty(tagNode.tag().tags); + return undefinedOrEmpty(tagNode.tag().tags) && !tagNode.nameNode; }); @@ -133,7 +137,7 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { }; node.tag = function () { - return tag; + return tag }; diff --git a/jgiven-html5-report/src/app/lib/util.js b/jgiven-html5-report/src/app/lib/util.js index 43917df478..be38066412 100644 --- a/jgiven-html5-report/src/app/lib/util.js +++ b/jgiven-html5-report/src/app/lib/util.js @@ -11,7 +11,7 @@ function getTagName(tag) { } function getTagKey(tag) { - return getTagName(tag) + '-' + tag.value; + return getTagName(tag) + (tag.value ? '-' + tag.value : ''); } function tagToString(tag) { diff --git a/jgiven-html5-report/src/test/app/tagServiceTest.js b/jgiven-html5-report/src/test/app/tagServiceTest.js index 87cc8e21bc..54258db618 100644 --- a/jgiven-html5-report/src/test/app/tagServiceTest.js +++ b/jgiven-html5-report/src/test/app/tagServiceTest.js @@ -1,105 +1,90 @@ describe("TagService", function () { beforeEach(module('jgivenReportApp')); - beforeEach(function () { - var tagFile = { - tagTypeMap: { - 'issue': { - 'type': 'issue', - 'tags': ['categoryA1'] - }, - 'categoryA': { - 'type': 'categoryA' - }, - 'categoryB': { - 'type': 'categoryB' - }, - 'categoryA1': { - 'type': 'categoryA1', - 'tags': ['categoryA'] - }, - 'featureA': { - 'type': 'featureA', - 'name': 'feature', - 'tags': ['categoryA'] - }, - 'featureB': { - 'type': 'featureB', - 'name': 'feature', - 'tags': ['categoryB'] - }, - 'somethingA': { - 'type': 'somethingA', - 'name': 'something' - } - }, - tags: { - 'issue-1': { - 'tagType': 'issue', - 'value': '1' - }, - 'issue-2': { - 'tagType': 'issue', - 'value': '2' - }, - 'categoryA': { - 'tagType': 'categoryA' - }, - 'categoryA1': { - 'tagType': 'categoryA1' - }, - 'categoryB': { - 'tagType': 'categoryB' - }, - 'featureA': { - 'tagType': 'featureA', - 'value': 'A' - }, - 'featureB': { - 'tagType': 'featureB', - 'value': 'B' - }, - 'somethingA': { - 'tagType': 'somethingA', - 'value': 'someA' - } - + function createDataServiceMock(tagFile, testCases) { + return { + getTagFile: function () { + return tagFile; } - }; - - var testCases = [{ - scenarios: [{ - tagIds: ['issue-1', 'issue-2', 'categoryB', 'featureA', 'featureB', 'somethingA'] - }] - }]; - - /** Expected Tag Tree: - + categoryA - --+ categoryA1 - ----+ issue - ------+ issue-1 - ------+ issue-2 - --+ featureA - + categoryB - --+ featureB - + feature - --+ featureA - --+ featureB - */ - + , + getTestCases: function () { + return testCases; + } + } + } - function createDataServiceMock(tagFile, testCases) { - return { - getTagFile: function () { - return tagFile; - } - , - getTestCases: function () { - return testCases; - } + var tagFile = { + tagTypeMap: { + 'issue': { + 'type': 'issue', + 'description': 'issue description' + }, + 'categoryA': { + 'type': 'categoryA' + }, + 'categoryB': { + 'type': 'categoryB' + }, + 'categoryA1': { + 'type': 'categoryA1', + 'tags': ['categoryA'] + }, + 'featureA': { + 'type': 'featureA', + 'name': 'feature', + 'tags': ['categoryA'] + }, + 'featureB': { + 'type': 'featureB', + 'name': 'feature', + 'tags': ['categoryB'] + }, + 'somethingA': { + 'type': 'somethingA', + 'name': 'something' + } + }, + tags: { + 'issue-1': { + 'tagType': 'issue', + 'value': '1' + }, + 'issue-2': { + 'tagType': 'issue', + 'value': '2' + }, + 'categoryA': { + 'tagType': 'categoryA' + }, + 'categoryA1': { + 'tagType': 'categoryA1' + }, + 'categoryB': { + 'tagType': 'categoryB' + }, + 'featureA': { + 'tagType': 'featureA', + 'value': 'A' + }, + 'featureB': { + 'tagType': 'featureB', + 'value': 'B' + }, + 'somethingA': { + 'tagType': 'somethingA', + 'value': 'someA' } + } + }; + + var testCases = [{ + scenarios: [{ + tagIds: ['issue-1', 'issue-2', 'categoryB', 'featureA', 'featureB', 'somethingA'] + }] + }]; + beforeEach(function () { module(function ($provide) { $provide.value('dataService', createDataServiceMock(tagFile, testCases)); }); @@ -112,11 +97,34 @@ describe("TagService", function () { })); it("calculates root tags correctly", function () { + + /** Expected Tag Tree: + + categoryA + --+ categoryA1 + --+ featureA + + categoryB + --+ featureB + + feature + --+ featureA + --+ featureB + + issue + --+ issue-1 + --+ issue-2 + */ + var rootTags = tagService.getRootTags(); expect(rootTags).toBeDefined(); var rootTagNames = _.map(rootTags, function (rootTag) { return rootTag.nodeName(); }); - expect(rootTagNames).toEqual(['categoryA', 'categoryB', 'feature', 'someA']); + expect(rootTagNames).toEqual(['categoryA', 'categoryB', 'feature', 'issue', 'someA']); }); + + it("getTagByKey", function () { + var issueTag = tagService.getTagByKey('issue-1'); + expect(issueTag).toBeDefined(); + expect(issueTag.type).toBe('issue'); + expect(issueTag.description).toEqual('issue description'); + }); + }); \ No newline at end of file diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java index 3b342168a4..2dcfeef19e 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java @@ -5,7 +5,7 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag +@IsTag( name = "Features" ) @Retention( RetentionPolicy.RUNTIME ) public @interface Feature { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAsciiDocReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAsciiDocReport.java index e2af72f66f..346c45dccb 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAsciiDocReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAsciiDocReport.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureReport -@IsTag( value = "AsciiDoc Report", +@IsTag( name = "AsciiDoc Report", description = "In order to easily combine hand-written documentation with JGiven scenarios
    " + "As a developer,
    " + "I want that JGiven generates AsciiDoc reports" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAttachments.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAttachments.java index adb497efcf..8364d79686 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAttachments.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAttachments.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureCore -@IsTag( value = "Attachments", +@IsTag( name = "Attachments", description = "In order to get additional information about a step, like screenshots, for example
    " + "As a JGiven user,
    " + "I want that steps can have attachments" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCaseDiffs.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCaseDiffs.java index f4d265006c..21f0ef0203 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCaseDiffs.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCaseDiffs.java @@ -5,8 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@FeatureHtml5Report -@IsTag( value = "Case Diffs", +@FeatureCore +@IsTag( name = "Case Diffs", description = "In order to get a better overview over structurally different cases of a scenario
    " + "As a human,
    " + "I want the differences highlighted in the generated report" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java index 99528c5bdd..b05524d7ce 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @Feature -@IsTag( value = "Core Features" ) +@IsTag( name = "Core Features" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureCore { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDataTables.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDataTables.java index 8b5168ea08..1c012c22ba 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDataTables.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDataTables.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureCore -@IsTag( value = "Data Tables", +@IsTag( name = "Data Tables", description = "In order to get a better overview over the different cases of a scenario
    " + "As a human,
    " + "I want to have different cases represented as a data table" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDerivedParameters.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDerivedParameters.java index a380603f73..d05204aa43 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDerivedParameters.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDerivedParameters.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureCore -@IsTag( value = "Derived Parameters", +@IsTag( name = "Derived Parameters", description = "In order to not have to specify easily derivable parameters explicitly
    " + "As a developer,
    " + "I want that step arguments derived from parameters appear in a data table" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDuration.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDuration.java index 3c0160e933..562a4a1f39 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDuration.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureDuration.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureCore -@IsTag( value = "Duration", description = "The duration of steps, cases, and scenarios is measured and reported" ) +@IsTag( name = "Duration", description = "The duration of steps, cases, and scenarios is measured and reported" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureDuration { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureGerman.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureGerman.java index 3ad8e3902b..7d3dda89ee 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureGerman.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureGerman.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureCore -@IsTag( value = "German Scenarios", description = "Scenarios can be written in German" ) +@IsTag( name = "German Scenarios", description = "Scenarios can be written in German" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureGerman { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java index 325a5463ac..4907d31123 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtml5Report.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureReport -@IsTag( value = "HTML5 Report", +@IsTag( name = "HTML5 Report", description = "In order to have an interactive JGiven report for non-developers
    " + "As a developer,
    " + "I want that JGiven generates HTML5 reports" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java index daf66fe7a4..31c7a51ef6 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureHtmlReport.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureReport -@IsTag( value = "Static HTML Report", +@IsTag( name = "Static HTML Report", description = "In order to show JGiven scenarios to non-developers
    " + "As a developer,
    " + "I want that JGiven generates static HTML reports" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureJUnit.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureJUnit.java index c7eb45f87d..0c221486f5 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureJUnit.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureJUnit.java @@ -5,9 +5,9 @@ import com.tngtech.jgiven.annotation.IsTag; -@Feature -@IsTag( value = "JUnit", - description = "tests can be be executed with JUnit" ) +@FeatureTestFramework +@IsTag( name = "JUnit", + description = "Tests can be be executed with JUnit" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureJUnit { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureNotImplementedYet.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureNotImplementedYet.java index c58d1e43d7..e36bf3e5df 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureNotImplementedYet.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureNotImplementedYet.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureCore -@IsTag( value = "NotImplementedYet Annotation", +@IsTag( name = "NotImplementedYet Annotation", description = "As a good BDD practitioner,
    " + "I want to write my scenarios before I start coding
    " + "In order to discuss them with business stakeholders" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java index d4ed88009e..9881900150 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @Feature -@IsTag( value = "Report Features" ) +@IsTag( name = "Reporting" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureReport { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureStepParameters.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureStepParameters.java index e4ce312ca1..f1a643f130 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureStepParameters.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureStepParameters.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureCore -@IsTag( value = "Step Parameters", description = "Steps can have parameters" ) +@IsTag( name = "Step Parameters", description = "Steps can have parameters" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureStepParameters { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTableStepArguments.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTableStepArguments.java index e7b96799b5..99e9bcc6ba 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTableStepArguments.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTableStepArguments.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureCore -@IsTag( value = "Table Step Arguments", +@IsTag( name = "Table Step Arguments", description = "In order to better present table-like data
    " + "As a human,
    " + "I want a special treatment of table-like data in step arguments" ) diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTags.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTags.java index 6d684b1e8a..cb6f1b189e 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTags.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTags.java @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Tags", description = "Scenarios can be tagged with annotations" ) +@FeatureCore +@IsTag( value = "Tags", description = "Scenarios can be tagged with annotations" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureTags { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestFramework.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestFramework.java new file mode 100644 index 0000000000..908f17d59c --- /dev/null +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestFramework.java @@ -0,0 +1,14 @@ +package com.tngtech.jgiven.tags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.tngtech.jgiven.annotation.IsTag; + +@Feature +@IsTag( name = "Supported Test Frameworks", + description = "JGiven can be used together with JUnit and TestNG" ) +@Retention( RetentionPolicy.RUNTIME ) +public @interface FeatureTestFramework { + +} diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestNg.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestNg.java index 43d6fbac41..e00fbe7f6e 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestNg.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTestNg.java @@ -5,9 +5,9 @@ import com.tngtech.jgiven.annotation.IsTag; -@Feature -@IsTag( value = "TestNG", - description = "tests can be be executed with TestNG" ) +@FeatureTestFramework +@IsTag( name = "TestNG", + description = "Tests can be be executed with TestNG" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureTestNg { diff --git a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java index fc5fe84522..57d70474bc 100644 --- a/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureTextReport.java @@ -6,7 +6,7 @@ import com.tngtech.jgiven.annotation.IsTag; @FeatureReport -@IsTag( value = "Text Report", description = "Plain text reports can be generated" ) +@IsTag( name = "Text Report", description = "Plain text reports can be generated" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureTextReport { From 3ddd930332ffa6ec2c119c206cc1486a2093a45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sat, 1 Aug 2015 22:35:23 +0200 Subject: [PATCH 06/13] execute karma headless and from gradle --- jgiven-html5-report/build.gradle | 7 +++++++ jgiven-html5-report/package.json | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/jgiven-html5-report/build.gradle b/jgiven-html5-report/build.gradle index 802fb777f3..84f09f7778 100644 --- a/jgiven-html5-report/build.gradle +++ b/jgiven-html5-report/build.gradle @@ -13,6 +13,13 @@ task bowerUpdate(type: Exec) { outputs.dir bowerComponentsDir } +task npmTest(type: Exec) { + executable = 'npm' + args = ['test'] +} + +test.finalizedBy(npmTest) + task copyFontFiles(type: Copy) { from bowerComponentsDir + 'fontawesome/fonts' into appDir + 'fonts' diff --git a/jgiven-html5-report/package.json b/jgiven-html5-report/package.json index fcaa31a1fe..046cb4aaf0 100644 --- a/jgiven-html5-report/package.json +++ b/jgiven-html5-report/package.json @@ -4,15 +4,17 @@ "description": "An HTML5 report for JGiven", "main": "src/app/app.js", "scripts": { - "test": "./node_modules/karma/bin/karma start --debug --single-run", - "test-watch": "./node_modules/karma/bin/karma start --debug" + "test": "./node_modules/karma/bin/karma start --debug --single-run --browsers PhantomJS", + "test-watch": "./node_modules/karma/bin/karma start --debug" }, "devDependencies": { "bower": "~1.3.12", "karma": "~0.13.3", "jasmine-core": "~2.3.4", "karma-jasmine": "~0.3.6", - "karma-chrome-launcher": "~0.2.0" + "karma-chrome-launcher": "~0.2.0", + "phantomjs": "~1.9.17", + "karma-phantomjs-launcher": "~0.2.0" }, "keywords": [ "jgiven", From e04f90a02ff4256c30910daefb19008f38f36f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sat, 1 Aug 2015 23:53:56 +0200 Subject: [PATCH 07/13] HTML5 Report: fix tag filtering --- jgiven-html5-report/src/app/lib/reportCtrl.js | 35 ++++++++++++++----- jgiven-html5-report/src/app/lib/tagService.js | 11 +++--- .../src/test/app/tagServiceTest.js | 9 ++++- .../jgiven/JGivenTestConfiguration.java | 7 ++-- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/jgiven-html5-report/src/app/lib/reportCtrl.js b/jgiven-html5-report/src/app/lib/reportCtrl.js index cfcc75e463..2b5f64bfe4 100644 --- a/jgiven-html5-report/src/app/lib/reportCtrl.js +++ b/jgiven-html5-report/src/app/lib/reportCtrl.js @@ -63,11 +63,12 @@ jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $ti if (part[1] === '') { $scope.showSummaryPage(); } else if (part[1] === 'tag') { - if (part[3]) { - var tag = tagService.getTagByKey(getTagKey({ - name: part[2], - value: part[3] - })); + var tag = tagService.getTagByKey(getTagKey({ + name: part[2], + value: part[3] + })); + + if (tag) { $scope.updateCurrentPageToTag(tag, selectedOptions); } else { var tagNameNode = tagService.getTagNameNode(part[2]); @@ -413,7 +414,7 @@ jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $ti function getFilterFunction(page) { var anyStatusMatches = anyOptionMatches(getSelectedOptions(page.options.statusOptions)); - var anyTagMatches = anyOptionMatches(getSelectedOptions(page.options.tagOptions)); + var anyTagMatches = allOptionMatches(getSelectedOptions(page.options.tagOptions)); var anyClassMatches = anyOptionMatches(getSelectedOptions(page.options.classOptions)); return function (scenario) { @@ -439,6 +440,24 @@ jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $ti } } + function allOptionMatches(filterOptions) { + // by default nothing is filtered + if (filterOptions.length === 0) { + return function () { + return true; + }; + } + + return function (scenario) { + for (var i = 0; i < filterOptions.length; i++) { + if (!filterOptions[i].apply(scenario)) { + return false; + } + } + return true; + } + } + function getSelectedSortOption(page) { return getSelectedOptions(page.options.sortOptions)[0]; } @@ -499,7 +518,6 @@ jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $ti }; $scope.isHeaderCell = function (rowIndex, columnIndex, headerType) { - console.log(headerType); if (rowIndex === 0 && (headerType === 'HORIZONTAL' || headerType === 'BOTH')) { return true; } @@ -521,4 +539,5 @@ jgivenReportApp.controller('JGivenReportCtrl', function ($scope, $rootScope, $ti $scope.init(); -}); +}) +; diff --git a/jgiven-html5-report/src/app/lib/tagService.js b/jgiven-html5-report/src/app/lib/tagService.js index 268ce51816..f42d9cb41d 100644 --- a/jgiven-html5-report/src/app/lib/tagService.js +++ b/jgiven-html5-report/src/app/lib/tagService.js @@ -28,7 +28,6 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { * that are tagged with that tag */ function getTagScenarioMap(scenarios) { - var tagEntry; var tagScenarioMap = {}; _.forEach(scenarios, function (testCase) { _.forEach(testCase.scenarios, function (scenario) { @@ -43,7 +42,7 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { function addEntry(tagId) { var tag = getTagByTagId(tagId); var tagKey = getTagKey(tag); - tagEntry = tagScenarioMap[tagKey]; + var tagEntry = tagScenarioMap[tagKey]; if (!tagEntry) { tagEntry = { tag: tag, @@ -51,7 +50,10 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { }; tagScenarioMap[tagKey] = tagEntry; } - tagEntry.scenarios.push(scenario); + + if (tagEntry.scenarios.indexOf(scenario) == -1) { + tagEntry.scenarios.push(scenario); + } _.forEach(tagEntry.tag.tags, function (tagId) { addEntry(tagId); @@ -210,7 +212,8 @@ jgivenReportApp.factory('tagService', ['dataService', function (dataService) { } function getTagByKey(tagKey) { - return tagScenarioMap[tagKey].tag; + var tagEntry = tagScenarioMap[tagKey]; + return tagEntry && tagEntry.tag; } function getTagNameNode(name) { diff --git a/jgiven-html5-report/src/test/app/tagServiceTest.js b/jgiven-html5-report/src/test/app/tagServiceTest.js index 54258db618..7975208976 100644 --- a/jgiven-html5-report/src/test/app/tagServiceTest.js +++ b/jgiven-html5-report/src/test/app/tagServiceTest.js @@ -120,11 +120,18 @@ describe("TagService", function () { expect(rootTagNames).toEqual(['categoryA', 'categoryB', 'feature', 'issue', 'someA']); }); - it("getTagByKey", function () { + it("return tags with descriptions", function () { var issueTag = tagService.getTagByKey('issue-1'); expect(issueTag).toBeDefined(); expect(issueTag.type).toBe('issue'); expect(issueTag.description).toEqual('issue description'); }); + it("returns scenarios with correct tags", function () { + var scenarios = tagService.getScenariosByTag(tagService.getTagByKey('issue-1')); + expect(scenarios.length).toEqual(1); + expect(scenarios[0].tags.length).toEqual(testCases[0].scenarios[0].tagIds.length); + expect(_.map(scenarios[0].tags, getTagKey)).toEqual(['issue-1', 'issue-2', 'categoryB', 'feature-A', 'feature-B', 'something-someA']); + }); + }); \ No newline at end of file diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/JGivenTestConfiguration.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/JGivenTestConfiguration.java index 256344d88a..28f67639f2 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/JGivenTestConfiguration.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/JGivenTestConfiguration.java @@ -8,9 +8,10 @@ public class JGivenTestConfiguration extends AbstractJGivenConfiguraton { @Override public void configure() { - configureTag(Issue.class) - .prependType(true) - .descriptionGenerator(IssueDescriptionGenerator.class); + configureTag( Issue.class ) + .prependType( true ) + .color( "orange" ) + .descriptionGenerator( IssueDescriptionGenerator.class ); } } From ef5ba5e772ebecb5a3fb0b84c55dfe17e7b49830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sun, 2 Aug 2015 00:08:39 +0200 Subject: [PATCH 08/13] HTML5 Report: do npm install before test to make travis happy --- jgiven-html5-report/build.gradle | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jgiven-html5-report/build.gradle b/jgiven-html5-report/build.gradle index 84f09f7778..52269a34a4 100644 --- a/jgiven-html5-report/build.gradle +++ b/jgiven-html5-report/build.gradle @@ -13,7 +13,12 @@ task bowerUpdate(type: Exec) { outputs.dir bowerComponentsDir } -task npmTest(type: Exec) { +task npmInstall(type: Exec) { + executable = 'npm' + args = ['install'] +} + +task npmTest(type: Exec, dependsOn: npmInstall) { executable = 'npm' args = ['test'] } From 87a702a9f1496e73566506b7a377d7592942e077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sun, 2 Aug 2015 00:24:25 +0200 Subject: [PATCH 09/13] fix description generator --- .../java/com/tngtech/jgiven/report/model/ReportModelBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java index d311744ee5..f10df9eb1c 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java @@ -485,6 +485,7 @@ private List getExplodedTags( Tag originalTag, Object[] values, Annotation for( Object singleValue : values ) { Tag newTag = originalTag.copy(); newTag.setValue( String.valueOf( singleValue ) ); + newTag.setDescription( getDescriptionFromGenerator( tagConfig, annotation, singleValue ) ); result.add( newTag ); } return result; From 9a0f4b6e53f0bc7ca39abbb115925704575850f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sun, 2 Aug 2015 14:21:02 +0200 Subject: [PATCH 10/13] HTML5 Report: fixed mobile navigation --- .../src/app/css/jgivenreport.css | 25 ++++++ jgiven-html5-report/src/app/index.html | 76 +++++++++---------- jgiven-html5-report/src/app/lib/app.js | 9 --- .../src/app/lib/offCanvasDirective.js | 60 +++++++++++++++ jgiven-html5-report/src/app/lib/tagService.js | 13 +++- jgiven-html5-report/src/app/lib/util.js | 10 +++ .../tngtech/jgiven/tags/FeatureReport.java | 3 +- 7 files changed, 144 insertions(+), 52 deletions(-) create mode 100644 jgiven-html5-report/src/app/lib/offCanvasDirective.js diff --git a/jgiven-html5-report/src/app/css/jgivenreport.css b/jgiven-html5-report/src/app/css/jgivenreport.css index 7862b0cad1..6ad5a34ac0 100644 --- a/jgiven-html5-report/src/app/css/jgivenreport.css +++ b/jgiven-html5-report/src/app/css/jgivenreport.css @@ -373,6 +373,18 @@ input.search { height: 1.7rem; } +.tab-bar-search-section { + width: 10rem; +} + +#nav-search-mobile { + height: 1.7rem; + margin-top: 0.5rem; + margin-right: 0.5rem; + margin-left: 0.5rem; + width: 14rem; +} + .search-icon { position: absolute; left: 1.5rem; @@ -380,6 +392,19 @@ input.search { color: #333; } +.tab-bar-search-section .search-icon { + left: 0rem; +} + +ul.off-canvas-list li label a { + border-bottom: none; + color: inherit; + display: inherit; + padding: inherit; + transition: none; + padding: 0; +} + .content { position: relative; background: white; diff --git a/jgiven-html5-report/src/app/index.html b/jgiven-html5-report/src/app/index.html index bb0459ad9c..7f79c02c4e 100644 --- a/jgiven-html5-report/src/app/index.html +++ b/jgiven-html5-report/src/app/index.html @@ -42,23 +42,19 @@

    - + + + @@ -110,37 +106,32 @@

    JGiven Report

    -
    +

    JGiven Report

    -
    + - +

    Loading

    @@ -234,7 +226,8 @@

    -
    +
    @@ -261,7 +254,7 @@

    Loading +
    {{currentPage.statistics.success}} Successful, Loading +
    @@ -49,10 +49,11 @@ diff --git a/jgiven-html5-report/src/app/lib/navigationCtrl.js b/jgiven-html5-report/src/app/lib/navigationCtrl.js index 378dfcdf05..e372bfd2be 100644 --- a/jgiven-html5-report/src/app/lib/navigationCtrl.js +++ b/jgiven-html5-report/src/app/lib/navigationCtrl.js @@ -16,5 +16,8 @@ jgivenReportApp.controller('JGivenNavigationCtrl', function ($scope, classServic */ $scope.rootPackage = classService.getRootPackage(); + $scope.orderNodes = function (node) { + return node.nodeName(); + }; }); From 9ef757a5fecd8c71eaa9e559801121fd38c2fd23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sun, 2 Aug 2015 14:47:26 +0200 Subject: [PATCH 13/13] add example to changelog --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 626ef2f1bc..5426c5f200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,18 @@ ### Hierarchical Tags -* Tags can now have parent tags by tagging a tag annotation. This allows you to define tag hierarchies. +Tags can now have parent tags by tagging a tag annotation. This allows you to define tag hierarchies. + +#### Example + +The following example tags the `FeatureHtml5Report` annotation with the `FeatureReport` annotation: + +``` +@FeatureReport +@IsTag( name = "HTML5 Report" ) +@Retention( RetentionPolicy.RUNTIME ) +public @interface FeatureHtml5Report { } +``` ### Enhanced Spring Support [#94](https://github.com/TNG/JGiven/pull/94)