diff --git a/CHANGELOG.md b/CHANGELOG.md index bc63574d05..5426c5f200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,28 @@ ## New Features +### Hierarchical Tags + +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) * 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 +32,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/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/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-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 e3f7d4ae9c..d161f7c9bf 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 @@ -33,7 +33,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..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 @@ -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,9 @@ 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() ); + Tag newTag = originalTag.copy(); + newTag.setValue( String.valueOf( singleValue ) ); newTag.setDescription( getDescriptionFromGenerator( tagConfig, annotation, singleValue ) ); - newTag.setColor( originalTag.getColor() ); - newTag.setCssClass( originalTag.getCssClass() ); 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/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/build.gradle b/jgiven-html5-report/build.gradle index 802fb777f3..52269a34a4 100644 --- a/jgiven-html5-report/build.gradle +++ b/jgiven-html5-report/build.gradle @@ -13,6 +13,18 @@ task bowerUpdate(type: Exec) { outputs.dir bowerComponentsDir } +task npmInstall(type: Exec) { + executable = 'npm' + args = ['install'] +} + +task npmTest(type: Exec, dependsOn: npmInstall) { + executable = 'npm' + args = ['test'] +} + +test.finalizedBy(npmTest) + task copyFontFiles(type: Copy) { from bowerComponentsDir + 'fontawesome/fonts' into appDir + 'fonts' 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 009aa4bf5c..046cb4aaf0 100644 --- a/jgiven-html5-report/package.json +++ b/jgiven-html5-report/package.json @@ -1,10 +1,20 @@ { "name": "jgiven-html5-report", - "version": "0.6.0", + "version": "0.7.4", "description": "An HTML5 report for JGiven", "main": "src/app/app.js", + "scripts": { + "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" + "bower": "~1.3.12", + "karma": "~0.13.3", + "jasmine-core": "~2.3.4", + "karma-jasmine": "~0.3.6", + "karma-chrome-launcher": "~0.2.0", + "phantomjs": "~1.9.17", + "karma-phantomjs-launcher": "~0.2.0" }, "keywords": [ "jgiven", 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..93949e7ab1 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; } @@ -376,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; @@ -383,11 +392,31 @@ input.search { color: #333; } +.tab-bar-search-section .search-icon { + left: 0rem; +} + +ul.off-canvas-list li.off-canvas-label > a { + background: #444; + border-bottom: none; + border-top: 1px solid #5e5e5e; + color: #999999; + font-size: 0.75rem; + font-weight: bold; + padding: 0.3rem 0.9375rem; + text-transform: uppercase; + margin: 0; +} + +ul.off-canvas-list li.off-canvas-label > a:hover { + background: #222; +} + .content { position: relative; background: white; padding-bottom: 5rem; - max-width:100%; + max-width: 100%; min-height: 100%; height: 100%; } @@ -446,16 +475,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 +506,6 @@ li.subheading { margin-top: 0.2rem; } - li.heading ul { text-transform: none; } @@ -495,24 +522,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 +554,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 +582,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 +600,7 @@ a.show-package-link:hover { } .off-canvas-wrap { - overflow: auto; + overflow: auto; } .page-statistics { @@ -591,7 +625,6 @@ a.show-package-link:hover { padding-left: 0; } - @media only screen and (max-width: 64em) { .scenario-container { margin-left: 0; @@ -607,19 +640,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..f514b462b2 100644 --- a/jgiven-html5-report/src/app/index.html +++ b/jgiven-html5-report/src/app/index.html @@ -1,298 +1,336 @@ - - - - JGiven Report - - - - - - - - - - - - - - - + + + + JGiven Report + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + +
    + + + -
    +
    -
    + - + - +
    +
  • +
  • All Scenarios
  • +
  • Failed Scenarios
  • +
  • Pending Scenarios
  • +
  • +
  • - +
  • +
  • + + - + + - +
    -
    + - +
    +
    +

    {{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 +340,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 +430,18 @@
    - + - ({{ nanosToSeconds(step.durationInNanos) }}s) + ({{ nanosToSeconds(step.durationInNanos) }}s)
    -
    FAILED: {{case.errorMessage}}
    +
    FAILED: {{case.errorMessage}} +
    @@ -383,15 +450,22 @@
    Cases
    - + - - + +
    #{{param}} + {{param}} + Status
    {{case.caseNr}}{{arg}} + {{arg}} + -
    {{case.errorMessage}}
    +
    + {{case.errorMessage}} +
    @@ -400,44 +474,59 @@
    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..db3742c9c9 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/app.js @@ -0,0 +1,13 @@ +'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; +}); + + 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..60813a2cdd --- /dev/null +++ b/jgiven-html5-report/src/app/lib/classService.js @@ -0,0 +1,154 @@ +/** + * 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'; + + /** + * 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 testCases = dataService.getTestCases(); + + _.forEach(testCases, function (testCase) { + classNameScenarioMap[testCase.className] = testCase; + }); + return classNameScenarioMap; + } + + function getTestCases() { + return dataService.getTestCases(); + } + + /** + * Builds up the navigation tree for classes + */ + function getRootPackage() { + var allClasses = getTestCases(); + 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); + }); + } + + function getTestCaseByClassName(className) { + return classNameScenarioMap[className]; + } + + return { + getTestCases: getTestCases, + getTestCaseByClassName: getTestCaseByClassName, + 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..cd182f4c29 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/dataService.js @@ -0,0 +1,74 @@ +/** + * Provides functions to access the generated scenario data + */ + +jgivenReportApp.factory('dataService', [function () { + 'use strict'; + + var tagFile = jgivenReport.tagFile; + var testCases = jgivenReport.scenarios; + + function getAllScenarios() { + return _.flatten(_.map(testCases, 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"; + }); + } + + return { + + getTagFile: function () { + return tagFile; + }, + + getTestCases: function () { + return testCases; + }, + + getAllScenarios: getAllScenarios, + getPendingScenarios: getPendingScenarios, + getFailedScenarios: getFailedScenarios, + + }; +}]); + +/** + * 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..e372bfd2be --- /dev/null +++ b/jgiven-html5-report/src/app/lib/navigationCtrl.js @@ -0,0 +1,23 @@ +/** + * 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(); + + $scope.orderNodes = function (node) { + return node.nodeName(); + }; + +}); diff --git a/jgiven-html5-report/src/app/lib/offCanvasDirective.js b/jgiven-html5-report/src/app/lib/offCanvasDirective.js new file mode 100644 index 0000000000..b8e5c898ee --- /dev/null +++ b/jgiven-html5-report/src/app/lib/offCanvasDirective.js @@ -0,0 +1,60 @@ +/** + * Implements multi-level menu which is not implemented yet in angular-foundation + */ + +jgivenReportApp.directive('hasSubmenu', [function () { + return { + require: '^offCanvasWrap', + restrict: 'C', + link: function ($scope, element, attrs) { + element.on('click', function (e) { + e.stopPropagation(); + angular.element(this.getElementsByClassName('left-submenu')[0]).addClass('move-right'); + angular.element(this.getElementsByClassName('right-submenu')[0]).addClass('move-left'); + }); + } + }; +}]); + + +jgivenReportApp.directive('offCanvasBack', [function () { + return { + restrict: 'C', + link: function ($scope, element) { + element.on('click', function (e) { + e.stopPropagation(); + angular.element(this.parentElement).removeClass('move-right'); + angular.element(this.parentElement).removeClass('move-left'); + }); + } + }; +}]); + +jgivenReportApp.directive('offCanvasClose', [function () { + return { + require: '^offCanvasWrap', + restrict: 'C', + link: function ($scope, element, attrs, offCanvasWrap) { + element.on('click', function (e) { + e.stopPropagation(); + offCanvasWrap.hide(); + }); + } + }; +}]); + +jgivenReportApp.directive('offCanvasCloseOnSubmit', [function () { + return { + require: '^offCanvasWrap', + restrict: 'C', + link: function ($scope, element, attrs, offCanvasWrap) { + element.on('click', function (e) { + e.stopPropagation(); + }); + element.on('submit', function (e) { + e.stopPropagation(); + offCanvasWrap.hide(); + }); + } + }; +}]); 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..2b5f64bfe4 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/reportCtrl.js @@ -0,0 +1,543 @@ +/** + * 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') { + var tag = tagService.getTagByKey(getTagKey({ + name: part[2], + value: part[3] + })); + + if (tag) { + $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(classService.getTestCaseByClassName(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(classService.getTestCaseByClassName(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 = allOptionMatches(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 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]; + } + + 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 $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); + }; + + $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) { + 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..06d2ac168a --- /dev/null +++ b/jgiven-html5-report/src/app/lib/tagService.js @@ -0,0 +1,250 @@ +/** + * 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.getTestCases()); + + /** + * Maps tag keys to tag nodes + */ + var tagNodeMap = {}; + + /** + * Maps tag names to list of tags with the same name + */ + var tagNameMap = {}; + + /** + * An array of root tags + */ + var rootTags; + + /** + * 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 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); + var tagEntry = tagScenarioMap[tagKey]; + if (!tagEntry) { + tagEntry = { + tag: tag, + scenarios: [] + }; + tagScenarioMap[tagKey] = tagEntry; + } + + if (tagEntry.scenarios.indexOf(scenario) == -1) { + tagEntry.scenarios.push(scenario); + } + + _.forEach(tagEntry.tag.tags, function (tagId) { + addEntry(tagId); + }); + + return tagEntry; + } + + }); + }); + }); + return tagScenarioMap; + } + + function getRootTags() { + if (!rootTags) { + rootTags = calculateRootTags(); + } + return rootTags; + } + + /** + * Builds up a hierarchy of tag nodes that is shown in the + * navigation and returns the list of root nodes + */ + function calculateRootTags() { + _.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); + }); + + 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) && !tagNode.nameNode; + }); + + + return _.sortBy(nameNodesWithMultipleEntries.concat(nodesWithoutParents), + function (tagNode) { + return tagNode.nodeName(); + }); + + + 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)) + + (tag.value ? '/' + window.encodeURIComponent(tag.value) : ''); + }; + + node.scenarios = function () { + return scenarios; + }; + + node.tag = function () { + return tag + }; + + + 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); + + 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 getTagByKey(tagKey) { + var tagEntry = tagScenarioMap[tagKey]; + return tagEntry && tagEntry.tag; + } + + function getTagNameNode(name) { + 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, + 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..4da4d09ec4 --- /dev/null +++ b/jgiven-html5-report/src/app/lib/util.js @@ -0,0 +1,99 @@ +/** + * Utility functions + */ + + +String.prototype.capitalize = function () { + return this.charAt(0).toUpperCase() + this.slice(1); +}; + +Array.prototype.pushArray = function (arr) { + this.push.apply(this, arr); +}; + + +function undefinedOrEmpty(array) { + return !array || array.length === 0; +} + +function getTagName(tag) { + return tag.name ? tag.name : tag.type; +} + +function getTagKey(tag) { + return getTagName(tag) + (tag.value ? '-' + 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-html5-report/src/test/app/tagServiceTest.js b/jgiven-html5-report/src/test/app/tagServiceTest.js new file mode 100644 index 0000000000..7975208976 --- /dev/null +++ b/jgiven-html5-report/src/test/app/tagServiceTest.js @@ -0,0 +1,137 @@ +describe("TagService", function () { + beforeEach(module('jgivenReportApp')); + + 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)); + }); + + + }); + + beforeEach(inject(function (_tagService_) { + tagService = _tagService_; + })); + + 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', 'issue', 'someA']); + }); + + 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-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/Feature.java b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.java new file mode 100644 index 0000000000..2dcfeef19e --- /dev/null +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/Feature.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( 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 583f34fee0..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "AsciiDoc Report", +@FeatureReport +@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 c39aaec6e7..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Attachments", +@FeatureCore +@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 20f7857b0d..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,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", 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 new file mode 100644 index 0000000000..b05524d7ce --- /dev/null +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureCore.java @@ -0,0 +1,13 @@ +package com.tngtech.jgiven.tags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.tngtech.jgiven.annotation.IsTag; + +@Feature +@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 a8c61f7902..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Data Tables", +@FeatureCore +@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 b711e81d3e..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Derived Parameters", +@FeatureCore +@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 4412556d35..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 @@ -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( 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 4af3c60006..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 @@ -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( 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 509e07d224..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "HTML5 Report", +@FeatureReport +@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 07b6744960..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Static HTML Report", +@FeatureReport +@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 7716796284..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,8 +5,9 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", 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 7dd55229a8..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "NotImplementedYet Annotation", +@FeatureCore +@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 new file mode 100644 index 0000000000..ead799c7e3 --- /dev/null +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureReport.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 = "Reporting", + description = "JGiven can generate text and HTML reports. An AsciiDoc report is currently under development." ) +@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..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Step Parameters", description = "Steps can have parameters" ) +@FeatureCore +@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 2a346176ed..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Table Step Arguments", +@FeatureCore +@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 9c1e4bf995..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,8 +5,9 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", 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 61e360d7b9..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 @@ -5,7 +5,8 @@ import com.tngtech.jgiven.annotation.IsTag; -@IsTag( type = "Feature", value = "Plain Text Report", description = "Plain text reports can be generated" ) +@FeatureReport +@IsTag( name = "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/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 ); } } 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 ) {