diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/CurrentStep.java b/jgiven-core/src/main/java/com/tngtech/jgiven/CurrentStep.java new file mode 100644 index 0000000000..b3bf09d4a2 --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/CurrentStep.java @@ -0,0 +1,26 @@ +package com.tngtech.jgiven; + +import com.tngtech.jgiven.attachment.Attachment; + +/** + * This interface can be injected into a stage by using the {@link com.tngtech.jgiven.annotation.ScenarioState} + * annotation. It provides programmatic access to the current executed step. + */ +public interface CurrentStep { + + /** + * Adds an attachment to the current step + * + * @param attachment an attachment to add + */ + void addAttachment( Attachment attachment ); + + /** + * Set the extended description of the current step + * + * @param extendedDescription the extended description + * @see com.tngtech.jgiven.annotation.ExtendedDescription + */ + void setExtendedDescription( String extendedDescription ); + +} diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/attachment/Attachment.java b/jgiven-core/src/main/java/com/tngtech/jgiven/attachment/Attachment.java new file mode 100644 index 0000000000..39c1dd656a --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/attachment/Attachment.java @@ -0,0 +1,182 @@ +package com.tngtech.jgiven.attachment; + +import java.io.*; +import java.nio.charset.Charset; + +import javax.xml.bind.DatatypeConverter; + +import com.google.common.io.ByteStreams; +import com.google.common.io.CharStreams; +import com.google.common.io.Files; +import com.tngtech.jgiven.impl.util.ResourceUtil; + +/** + * Represents an attachment of a step. + * Attachments must be representable as a String so that it can be stored as JSON. + * For binary attachments this means that they have to be encoded with Base64. + * In addition, attachments must have a media type so that reporters know + * how to present an attachment. + * + */ +public class Attachment { + + /** + * An optional title. + * Can be {@code null}. + */ + private String title; + + /** + * The content of the attachment. + * In case the media type is binary, this is a Base64 encoded string + */ + private final String content; + + private final MediaType mediaType; + + /** + * Convenience constructor, where title is set to {@code null} + */ + protected Attachment( String content, MediaType mediaType ) { + this( content, mediaType, null ); + } + + /** + * Creates a new instance of this Attachment + * @param content the content of this attachment. In case of a binary attachment, this must be + * Base64 encoded. Must not be {@code null} + * @param mediaType the mediaType. Must not be {@code null} + * @param title an optional title, may be {@code null} + */ + protected Attachment( String content, MediaType mediaType, String title ) { + if( mediaType == null ) { + throw new IllegalArgumentException( "MediaType must not be null" ); + } + + if( content == null ) { + throw new IllegalArgumentException( "Content must not be null" ); + } + + this.content = content; + this.mediaType = mediaType; + this.title = title; + } + + /** + * An optional title of the attachment. + * The title can be used by reporters, e.g. as a tooltip. + * Can be {@code null}. + */ + public String title() { + return title; + } + + /** + * The content of the attachment represented as a string. + * Binary attachments must be encoded in Base64 format. + */ + public String content() { + return content; + } + + /** + * The type of the attachment. + * It depends on the reporter how this information is used. + */ + public MediaType getMediaType() { + return mediaType; + } + + /** + * An optional title + */ + public String getTitle() { + return title; + } + + /** + * Sets the title and returns {@code this} + */ + public Attachment withTitle( String title ) { + this.title = title; + return this; + } + + /** + * Creates an attachment from a given array of bytes. + * The bytes will be Base64 encoded. + * @throws java.lang.IllegalArgumentException if mediaType is not binary + */ + public static Attachment fromBinaryBytes( byte[] bytes, MediaType mediaType ) { + if( !mediaType.isBinary() ) { + throw new IllegalArgumentException( "MediaType must be binary" ); + } + return new Attachment( DatatypeConverter.printBase64Binary( bytes ), mediaType, null ); + } + + /** + * Creates an attachment from a binary input stream. + * The content of the stream will be transformed into a Base64 encoded string + * @throws IOException if an I/O error occurs + * @throws java.lang.IllegalArgumentException if mediaType is not binary + */ + public static Attachment fromBinaryInputStream( InputStream inputStream, MediaType mediaType ) throws IOException { + return fromBinaryBytes( ByteStreams.toByteArray( inputStream ), mediaType ); + } + + /** + * Creates an attachment from the given binary file {@code file}. + * The content of the file will be transformed into a Base64 encoded string. + * @throws IOException if an I/O error occurs + * @throws java.lang.IllegalArgumentException if mediaType is not binary + */ + public static Attachment fromBinaryFile( File file, MediaType mediaType ) throws IOException { + FileInputStream stream = new FileInputStream( file ); + try { + return fromBinaryInputStream( stream, mediaType ); + } finally { + ResourceUtil.close( stream ); + } + } + + /** + * Creates a non-binary attachment from the given file. + * @throws IOException if an I/O error occurs + * @throws java.lang.IllegalArgumentException if mediaType is binary + */ + public static Attachment fromTextFile( File file, MediaType mediaType, Charset charSet ) throws IOException { + return fromText( Files.toString( file, charSet ), mediaType ); + } + + /** + * Creates a non-binary attachment from the given file. + * @throws IOException if an I/O error occurs + * @throws java.lang.IllegalArgumentException if mediaType is binary + */ + public static Attachment fromTextInputStream( InputStream inputStream, MediaType mediaType, Charset charset ) throws IOException { + return fromText( CharStreams.toString( new InputStreamReader( inputStream, charset ) ), mediaType ); + } + + /** + * Equivalent to {@link com.tngtech.jgiven.attachment.Attachment#Attachment(String, MediaType)} + * @throws java.lang.IllegalArgumentException if mediaType is binary + */ + public static Attachment fromText( String content, MediaType mediaType ) { + if( mediaType.isBinary() ) { + throw new IllegalArgumentException( "MediaType must not be binary" ); + } + return new Attachment( content, mediaType ); + } + + /** + * Equivalent to {@link com.tngtech.jgiven.attachment.Attachment#Attachment(String, MediaType)} + * @throws java.lang.IllegalArgumentException if mediaType is not binary + */ + public static Attachment fromBase64( String base64encodedContent, MediaType mediaType ) { + if( !mediaType.isBinary() ) { + throw new IllegalArgumentException( "MediaType must be binary" ); + } + return new Attachment( base64encodedContent, mediaType ); + } + +} diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/attachment/MediaType.java b/jgiven-core/src/main/java/com/tngtech/jgiven/attachment/MediaType.java new file mode 100644 index 0000000000..b8436d397d --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/attachment/MediaType.java @@ -0,0 +1,118 @@ +package com.tngtech.jgiven.attachment; + +import static com.tngtech.jgiven.attachment.MediaType.Type.*; + +/** + * Represents a Media Type. + */ +public class MediaType { + + /** + * Represents the type of a Media Type + */ + public static enum Type { + APPLICATION( "application" ), + AUDIO( "audio" ), + IMAGE( "image" ), + TEXT( "text" ), + VIDEO( "video" ); + + private final String value; + + Type( String value ) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + + /** + * Get the type from a given string + */ + public Type fromString( String string ) { + for( Type type : values() ) { + if( type.value.equalsIgnoreCase( string ) ) { + return type; + } + } + throw new IllegalArgumentException( "Unknown type " + string ); + } + + } + + public static final MediaType PNG = image( "png" ); + public static final MediaType PLAIN_TEXT = text( "plain" ); + + private final Type type; + private final String subType; + private final boolean binary; + + /** + * Creates a new MediaType + * @param type the type + * @param subType the subtype + * @param binary whether or not content of this media type is binary. If {@code true}, the + * content will be encoded as Base64 when stored in the JSON model. + */ + public MediaType( Type type, String subType, boolean binary ) { + this.type = type; + this.subType = subType; + this.binary = binary; + } + + /** + * The type of the Media Type. + */ + public Type getType() { + return type; + } + + /** + * The subtype of the Media Type. + */ + public String getSubType() { + return subType; + } + + /** + * Whether this media type is binary or not. + */ + public boolean isBinary() { + return binary; + } + + public String asString() { + return type.value + "/" + subType; + } + + /** + * Creates a binary image media type with the given subtype. + */ + public static MediaType image( String subType ) { + return new MediaType( IMAGE, subType, true ); + } + + /** + * Creates a binary video media type with the given subtype. + */ + public static MediaType video( String subType ) { + return new MediaType( VIDEO, subType, true ); + } + + /** + * Creates a binary audio media type with the given subtype. + */ + public static MediaType audio( String subType ) { + return new MediaType( AUDIO, subType, true ); + } + + /** + * Creates a non-binary text media type with the given subtype. + */ + public static MediaType text( String subType ) { + return new MediaType( TEXT, subType, false ); + } + +} diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java index 6cec2fe836..356daf59eb 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java @@ -1,8 +1,10 @@ package com.tngtech.jgiven.impl; -import static com.google.common.collect.Lists.*; -import static com.tngtech.jgiven.impl.ScenarioExecutor.State.*; -import static com.tngtech.jgiven.impl.util.ReflectionUtil.*; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Lists.reverse; +import static com.tngtech.jgiven.impl.ScenarioExecutor.State.FINISHED; +import static com.tngtech.jgiven.impl.ScenarioExecutor.State.STARTED; +import static com.tngtech.jgiven.impl.util.ReflectionUtil.hasAtLeastOneAnnotation; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -18,25 +20,16 @@ import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import com.tngtech.jgiven.annotation.AfterScenario; -import com.tngtech.jgiven.annotation.AfterStage; -import com.tngtech.jgiven.annotation.BeforeScenario; -import com.tngtech.jgiven.annotation.BeforeStage; -import com.tngtech.jgiven.annotation.Hidden; -import com.tngtech.jgiven.annotation.IntroWord; -import com.tngtech.jgiven.annotation.NotImplementedYet; -import com.tngtech.jgiven.annotation.ScenarioRule; -import com.tngtech.jgiven.annotation.ScenarioStage; +import com.tngtech.jgiven.CurrentStep; +import com.tngtech.jgiven.annotation.*; +import com.tngtech.jgiven.attachment.Attachment; import com.tngtech.jgiven.exception.FailIfPassedException; import com.tngtech.jgiven.exception.JGivenUserException; import com.tngtech.jgiven.impl.inject.ValueInjector; -import com.tngtech.jgiven.impl.intercept.InvocationMode; -import com.tngtech.jgiven.impl.intercept.NoOpScenarioListener; -import com.tngtech.jgiven.impl.intercept.ScenarioListener; -import com.tngtech.jgiven.impl.intercept.StepMethodHandler; -import com.tngtech.jgiven.impl.intercept.StepMethodInterceptor; +import com.tngtech.jgiven.impl.intercept.*; import com.tngtech.jgiven.impl.util.ReflectionUtil; -import com.tngtech.jgiven.impl.util.ReflectionUtil.*; +import com.tngtech.jgiven.impl.util.ReflectionUtil.FieldAction; +import com.tngtech.jgiven.impl.util.ReflectionUtil.MethodAction; import com.tngtech.jgiven.impl.util.ScenarioUtil; import com.tngtech.jgiven.integration.CanWire; import com.tngtech.jgiven.report.model.NamedArgument; @@ -85,6 +78,7 @@ public enum State { public ScenarioExecutor() { injector.injectValueByType( ScenarioExecutor.class, this ); + injector.injectValueByType( CurrentStep.class, new StepAccessImpl() ); } static class StageState { @@ -97,6 +91,19 @@ static class StageState { } } + class StepAccessImpl implements CurrentStep { + + @Override + public void addAttachment( Attachment attachment ) { + listener.attachmentAdded( attachment ); + } + + @Override + public void setExtendedDescription( String extendedDescription ) { + listener.extendedDescriptionUpdated( extendedDescription ); + } + } + class MethodHandler implements StepMethodHandler { @Override public void handleMethod( Object stageInstance, Method paramMethod, Object[] arguments, InvocationMode mode ) diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/NoOpScenarioListener.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/NoOpScenarioListener.java index 37bcd1f3f7..b29434680f 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/NoOpScenarioListener.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/NoOpScenarioListener.java @@ -3,6 +3,7 @@ import java.lang.reflect.Method; import java.util.List; +import com.tngtech.jgiven.attachment.Attachment; import com.tngtech.jgiven.report.model.NamedArgument; public class NoOpScenarioListener implements ScenarioListener { @@ -30,4 +31,10 @@ public void stepMethodFinished( long durationInNanos ) {} @Override public void scenarioFinished() {} + + @Override + public void attachmentAdded( Attachment attachment ) {} + + @Override + public void extendedDescriptionUpdated( String extendedDescription ) {} } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/ScenarioListener.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/ScenarioListener.java index 748f3f918b..b86bd8328e 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/ScenarioListener.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/ScenarioListener.java @@ -3,6 +3,7 @@ import java.lang.reflect.Method; import java.util.List; +import com.tngtech.jgiven.attachment.Attachment; import com.tngtech.jgiven.report.model.NamedArgument; public interface ScenarioListener { @@ -22,4 +23,8 @@ public interface ScenarioListener { void stepMethodFinished( long durationInNanos ); void scenarioFinished(); + + void attachmentAdded( Attachment attachment ); + + void extendedDescriptionUpdated( String extendedDescription ); } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/AttachmentModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/AttachmentModel.java new file mode 100644 index 0000000000..6392f81b64 --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/AttachmentModel.java @@ -0,0 +1,40 @@ +package com.tngtech.jgiven.report.model; + +public class AttachmentModel { + private String title; + private String value; + private String mediaType; + private boolean binary; + + public String getValue() { + return value; + } + + public void setValue( String value ) { + this.value = value; + } + + public String getMediaType() { + return mediaType; + } + + public void setMediaType( String mimeType ) { + this.mediaType = mimeType; + } + + public void setTitle( String title ) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public void setIsBinary( boolean isBinary ) { + this.binary = isBinary; + } + + public boolean isBinary() { + return binary; + } +} 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 a28293fb9d..21e08431ce 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 @@ -15,6 +15,7 @@ import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.tngtech.jgiven.annotation.*; +import com.tngtech.jgiven.attachment.Attachment; import com.tngtech.jgiven.config.AbstractJGivenConfiguraton; import com.tngtech.jgiven.config.ConfigurationUtil; import com.tngtech.jgiven.config.DefaultConfiguration; @@ -37,6 +38,7 @@ public class ReportModelBuilder implements ScenarioListener { private ScenarioModel currentScenarioModel; private ScenarioCaseModel currentScenarioCase; + private StepModel currentStep; private ReportModel reportModel; private Word introWord; @@ -113,6 +115,7 @@ public void addStepMethod( Method paramMethod, List arguments, In } stepModel.setStatus( mode.toStepStatus() ); + currentStep = stepModel; writeStep( stepModel ); } @@ -405,6 +408,16 @@ public void scenarioFinished() { currentScenarioModel.addDurationInNanos( durationInNanos ); } + @Override + public void attachmentAdded( Attachment attachment ) { + currentStep.setAttachment( attachment ); + } + + @Override + public void extendedDescriptionUpdated( String extendedDescription ) { + currentStep.setExtendedDescription( extendedDescription ); + } + public void setTestClass( Class testClass ) { setClassName( testClass.getName() ); if( testClass.isAnnotationPresent( Description.class ) ) { diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/StepModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/StepModel.java index 2a78e7a123..c184f77f6d 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/StepModel.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/StepModel.java @@ -6,6 +6,7 @@ import com.google.common.base.Joiner; import com.google.common.collect.Lists; +import com.tngtech.jgiven.attachment.Attachment; public class StepModel { /** @@ -34,6 +35,11 @@ public class StepModel { */ private String extendedDescription; + /** + * An optional attachment of the step + */ + private AttachmentModel attachment; + public StepModel() {} public StepModel( String name, List words ) { @@ -105,4 +111,16 @@ public Iterable getWords() { public Word getLastWord() { return words.get( words.size() - 1 ); } + + public void setAttachment( Attachment attachment ) { + this.attachment = new AttachmentModel(); + this.attachment.setTitle(attachment.getTitle()); + this.attachment.setValue( attachment.content() ); + this.attachment.setMediaType(attachment.getMediaType().asString()); + this.attachment.setIsBinary( attachment.getMediaType().isBinary()); + } + + public AttachmentModel getAttachment() { + return attachment; + } } \ No newline at end of file diff --git a/jgiven-html5-report/src/app/index.html b/jgiven-html5-report/src/app/index.html index 08ec2d83ea..d35142bab5 100644 --- a/jgiven-html5-report/src/app/index.html +++ b/jgiven-html5-report/src/app/index.html @@ -258,6 +258,14 @@
+ + + + diff --git a/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/Html5AttachmentGenerator.java b/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/Html5AttachmentGenerator.java new file mode 100644 index 0000000000..800bff0add --- /dev/null +++ b/jgiven-html5-report/src/main/java/com/tngtech/jgiven/report/html5/Html5AttachmentGenerator.java @@ -0,0 +1,91 @@ +package com.tngtech.jgiven.report.html5; + +import static javax.xml.bind.DatatypeConverter.parseBase64Binary; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import com.google.common.net.MediaType; +import com.tngtech.jgiven.exception.JGivenInstallationException; +import com.tngtech.jgiven.report.model.AttachmentModel; +import com.tngtech.jgiven.report.model.ReportModel; +import com.tngtech.jgiven.report.model.ReportModelVisitor; +import com.tngtech.jgiven.report.model.StepModel; + +class Html5AttachmentGenerator extends ReportModelVisitor { + private static final Logger log = LoggerFactory.getLogger( Html5AttachmentGenerator.class ); + private static final String ATTACHMENT_DIRNAME = "attachments"; + + private int fileCounter; + private File attachmentsDir; + + public void generateAttachments( File targetDir, ReportModel model ) { + attachmentsDir = new File( targetDir, ATTACHMENT_DIRNAME ); + if( !attachmentsDir.exists() && !attachmentsDir.mkdirs() ) { + throw new JGivenInstallationException( "Could not create directory " + attachmentsDir ); + } + model.accept( this ); + } + + @Override + public void visit( StepModel stepModel ) { + AttachmentModel attachment = stepModel.getAttachment(); + if( attachment == null ) { + return; + } + + String mimeType = attachment.getMediaType(); + MediaType mediaType = MediaType.parse( mimeType ); + File targetFile = null; + if( mediaType.is( MediaType.ANY_TEXT_TYPE ) ) { + targetFile = writeTextFile( attachment ); + } else if( mediaType.is( MediaType.ANY_IMAGE_TYPE ) ) { + targetFile = writeImageFile( attachment, mediaType ); + } + + if( targetFile != null ) { + attachment.setValue( ATTACHMENT_DIRNAME + "/" + targetFile.getName() ); + } else { + attachment.setValue( null ); + } + log.info( "Attachment written to " + targetFile ); + } + + private File writeImageFile( AttachmentModel attachment, MediaType mediaType ) { + if( !mediaType.is( MediaType.PNG ) ) { + log.error( "Mime type " + mediaType + " is not supported as an image attachment. Only PNG is supported." ); + } + + String extension = "png"; + File targetFile = getTargetFile( extension ); + try { + Files.write( parseBase64Binary( attachment.getValue() ), targetFile ); + } catch( IOException e ) { + log.error( "Error while trying to write attachment to file " + targetFile, e ); + } + return targetFile; + } + + private File getTargetFile( String extension ) { + return new File( attachmentsDir, "attachment" + getNextFileCounter() + "." + extension ); + } + + private File writeTextFile( AttachmentModel attachment ) { + File targetFile = getTargetFile( "txt" ); + try { + Files.write( attachment.getValue(), targetFile, Charsets.UTF_8 ); + } catch( IOException e ) { + log.error( "Error while trying to write attachment to file " + targetFile, e ); + } + return targetFile; + } + + private int getNextFileCounter() { + return fileCounter++; + } +} 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 56923407df..7bc7d95a07 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 @@ -32,6 +32,7 @@ public class Html5ReportGenerator implements ReportModelFileHandler, FileGenerat @Override public void handleReportModel( ReportModel model, File file ) { model.calculateExecutionStatus(); + new Html5AttachmentGenerator().generateAttachments( targetDirectory, model ); createWriter(); 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 new file mode 100644 index 0000000000..c39aaec6e7 --- /dev/null +++ b/jgiven-tests/src/main/java/com/tngtech/jgiven/tags/FeatureAttachments.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; + +@IsTag( type = "Feature", value = "Attachments", + description = "In order to get additional information about a step, like screenshots, for example
" + + "As a JGiven user,
" + + "I want that steps can have attachments" ) +@Retention( RetentionPolicy.RUNTIME ) +public @interface FeatureAttachments {} 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 a2cd67b82a..20f7857b0d 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 @@ -7,7 +7,7 @@ @IsTag( type = "Feature", value = "Case Diffs", description = "In order to get a better overview over structurally different cases of a scenario
" - + "As a human,
" + + "As a human,
" + "I want the differences highlighted in the generated report" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureCaseDiffs { 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 f7e2dd2d73..a8c61f7902 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 @@ -7,7 +7,7 @@ @IsTag( type = "Feature", value = "Data Tables", description = "In order to get a better overview over the different cases of a scenario
" - + "As a human,
" + + "As a human,
" + "I want to have different cases represented as a data table" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureDataTables { 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 2c5749e014..b711e81d3e 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 @@ -7,7 +7,7 @@ @IsTag( type = "Feature", value = "Derived Parameters", description = "In order to not have to specify easily derivable parameters explicitly
" - + "As a developer,
" + + "As a developer,
" + "I want that step arguments derived from parameters appear in a data table" ) @Retention( RetentionPolicy.RUNTIME ) public @interface FeatureDerivedParameters { 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/Html5GeneratorTest.java index b18078b070..13c5f48d73 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/Html5GeneratorTest.java @@ -10,6 +10,7 @@ import com.tngtech.jgiven.report.WhenReportGenerator; import com.tngtech.jgiven.report.json.GivenJsonReports; import com.tngtech.jgiven.report.model.StepStatus; +import com.tngtech.jgiven.tags.FeatureAttachments; import com.tngtech.jgiven.tags.FeatureHtml5Report; import com.tngtech.jgiven.tags.Issue; @@ -67,4 +68,23 @@ public void clicking_on_tag_labels_opens_the_tag_page( boolean prependType, Stri then().the_page_title_is( tagName ); } + + @Test + @FeatureAttachments + public void attachments_appear_in_the_HTML5_report() throws Exception { + String content = "Some Example Attachment\nwith some example content"; + given().a_report_model() + .and().step_$_of_scenario_$_has_a_text_attachment_with_content( 1, 1, content ) + .and().the_report_exist_as_JSON_file(); + + whenReport + .and().the_HTML5_report_has_been_generated(); + + when().the_page_of_scenario_$_is_opened( 1 ) + .and().scenario_$_is_expanded( 1 ); + + then().an_attachment_icon_exists() + .and().the_content_of_the_referenced_attachment_is( content ); + + } } 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/Html5ReportStage.java new file mode 100644 index 0000000000..96bea2912f --- /dev/null +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html5/Html5ReportStage.java @@ -0,0 +1,30 @@ +package com.tngtech.jgiven.report.html5; + +import java.io.File; + +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; + +import com.tngtech.jgiven.CurrentStep; +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.attachment.Attachment; +import com.tngtech.jgiven.attachment.MediaType; + +public class Html5ReportStage> extends Stage { + @ExpectedScenarioState + protected CurrentStep currentStep; + + @ExpectedScenarioState + protected WebDriver webDriver; + + @ExpectedScenarioState + protected File targetReportDir; + + protected void takeScreenshot() { + String base64 = ( (TakesScreenshot) webDriver ).getScreenshotAs( OutputType.BASE64 ); + currentStep.addAttachment( Attachment.fromBase64( base64, MediaType.PNG ).withTitle( "Screenshot" ) ); + } + +} 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/ThenHtml5Report.java index 007afbdac9..c9a907ad02 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/ThenHtml5Report.java @@ -1,25 +1,42 @@ package com.tngtech.jgiven.report.html5; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; - -import com.tngtech.jgiven.Stage; -import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; -public class ThenHtml5Report> extends Stage { +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.testng.reporters.Files; - @ExpectedScenarioState - protected WebDriver webDriver; +public class ThenHtml5Report> extends Html5ReportStage { public SELF the_page_title_is( String pageTitle ) { assertThat( webDriver.findElement( By.id( "page-title" ) ).getText() ).isEqualTo( pageTitle ); return self(); } - public SELF the_page_statistics_line_contains_text( String text ) { + public SELF the_page_statistics_line_contains_text( String text ) throws IOException { assertThat( webDriver.findElement( By.className( "page-statistics" ) ).getText() ).contains( text ); return self(); } + + public SELF an_attachment_icon_exists() { + assertThat( findAttachmentIcon() ).isNotEmpty(); + return self(); + } + + private List findAttachmentIcon() { + return webDriver.findElements( By.className( "fa-paperclip" ) ); + } + + public SELF the_content_of_the_referenced_attachment_is( String content ) throws IOException, URISyntaxException { + String href = findAttachmentIcon().get( 0 ).findElement( By.xpath( ".." ) ).getAttribute( "href" ); + String foundContent = Files.readFile( new File( new URL( href ).toURI() ) ).trim(); + assertThat( content ).isEqualTo( foundContent ); + return self(); + } } 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/WhenHtml5Report.java index 2b88f89fe8..864ac04b07 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/WhenHtml5Report.java @@ -5,27 +5,34 @@ import java.util.List; import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; +import org.openqa.selenium.Dimension; import org.openqa.selenium.WebElement; import org.openqa.selenium.phantomjs.PhantomJSDriver; -import com.tngtech.jgiven.Stage; import com.tngtech.jgiven.annotation.AfterScenario; -import com.tngtech.jgiven.annotation.ProvidedScenarioState; +import com.tngtech.jgiven.annotation.AfterStage; +import com.tngtech.jgiven.annotation.BeforeScenario; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.impl.util.WordUtil; +import com.tngtech.jgiven.report.model.ReportModel; +import com.tngtech.jgiven.report.model.ScenarioModel; -public class WhenHtml5Report> extends Stage { +public class WhenHtml5Report> extends Html5ReportStage { - @ProvidedScenarioState - protected WebDriver webDriver = new PhantomJSDriver(); - - @ProvidedScenarioState - protected File targetReportDir; + @ExpectedScenarioState + protected List reportModels; public SELF the_index_page_is_opened() throws MalformedURLException { url_$_is_opened( "" ); return self(); } + @BeforeScenario + protected void setupWebDriver() { + webDriver = new PhantomJSDriver(); + webDriver.manage().window().setSize( new Dimension( 1280, 768 ) ); + } + @AfterScenario protected void closeWebDriver() { webDriver.close(); @@ -55,4 +62,31 @@ public SELF the_All_Scenarios_page_is_opened() throws MalformedURLException { } return self(); } + + public SELF scenario_$_is_expanded( int scenarioNr ) { + ScenarioModel scenarioModel = getScenarioModel( scenarioNr ); + webDriver.findElement( By.xpath( "//h4[contains(text(),'" + + WordUtil.capitalize( scenarioModel.getDescription() ) + "')]" ) ) + .click(); + return self(); + } + + private ScenarioModel getScenarioModel( int scenarioNr ) { + return reportModels.get( 0 ).getScenarios().get( scenarioNr - 1 ); + } + + public SELF the_page_of_scenario_$_is_opened( int scenarioNr ) throws MalformedURLException { + + ScenarioModel scenarioModel = getScenarioModel( scenarioNr ); + url_$_is_opened( "#/scenario/" + + scenarioModel.getClassName() + + "/" + scenarioModel.getTestMethodName() ); + return self(); + } + + @AfterStage + public void takeScreenshotAfterStage() { + takeScreenshot(); + + } } 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 81fc852035..402b1375e2 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 @@ -11,6 +11,8 @@ import com.tngtech.jgiven.annotation.ExtendedDescription; import com.tngtech.jgiven.annotation.ProvidedScenarioState; import com.tngtech.jgiven.annotation.Table; +import com.tngtech.jgiven.attachment.Attachment; +import com.tngtech.jgiven.attachment.MediaType; import com.tngtech.jgiven.report.analysis.CaseArgumentAnalyser; public class GivenReportModel> extends Stage { @@ -207,8 +209,8 @@ private DataTable toDataTable( String[][] dataTable ) { public SELF case_$_has_a_when_step_$_with_argument_$_and_argument_name_$( int ncase, String name, String arg, String argName ) { getCase( ncase ) .addStep( - new StepModel(name, - Arrays.asList(Word.introWord("when"), new Word(name), Word.argWord(argName, arg, (String) null)))); + new StepModel( name, + Arrays.asList( Word.introWord( "when" ), new Word( name ), Word.argWord( argName, arg, (String) null ) ) ) ); return self(); } @@ -239,4 +241,10 @@ public SELF header_type_set_to( Table.HeaderType headerType ) { latestWord.getArgumentInfo().getDataTable().setHeaderType( headerType ); return self(); } + + public SELF step_$_of_scenario_$_has_an_attachment_with_content( int stepNr, int scenarioNr, String content ) { + StepModel step = reportModel.getScenarios().get( scenarioNr - 1 ).getScenarioCases().get( 0 ).getStep( stepNr - 1 ); + step.setAttachment( Attachment.fromText( content, MediaType.PLAIN_TEXT ) ); + return self(); + } } diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModels.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModels.java index 7ef377524d..e98ddb9d42 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModels.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModels.java @@ -66,4 +66,9 @@ public SELF the_tag_has_prependType_set_to( boolean prependType ) { givenReportModel.step_$_of_case_$_has_status( stepNr, caseNr, status ); return self(); } + + public SELF step_$_of_scenario_$_has_a_text_attachment_with_content(int stepNr, int scenarioNr, String content) { + givenReportModel.step_$_of_scenario_$_has_an_attachment_with_content(stepNr, scenarioNr,content); + return self(); + } }