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();
+ }
}