Blob Blame History Raw
From eab8136d73e9c17ed61a099170ef7ed71788e376 Mon Sep 17 00:00:00 2001
From: Vadim Rutkovsky <vrutkovs@redhat.com>
Date: Thu, 17 Apr 2014 10:53:39 +0200
Subject: [PATCH] HTML Formatter

---
 behave/compat/collections.py     | 185 ++++++++++
 behave/configuration.py          |   8 +-
 behave/formatter/_builtins.py    |   1 +
 behave/formatter/behave.css      | 241 +++++++++++++
 behave/formatter/html.py         | 496 ++++++++++++++++++++++++++
 behave/runner.py                 |   5 +
 features/formatter.help.feature  |   1 +
 features/formatter.html.feature  | 749 +++++++++++++++++++++++++++++++++++++++
 issue.features/issue0031.feature |  11 +
 9 files changed, 1696 insertions(+), 1 deletion(-)
 create mode 100644 behave/formatter/behave.css
 create mode 100644 behave/formatter/html.py
 create mode 100644 features/formatter.html.feature

diff --git a/behave/compat/collections.py b/behave/compat/collections.py
index 530578c..cc27448 100644
--- a/behave/compat/collections.py
+++ b/behave/compat/collections.py
@@ -18,3 +18,188 @@ except ImportError:     # pragma: no cover
         warnings.warn(message)
         # -- BACKWARD-COMPATIBLE: Better than nothing (for behave use case).
         OrderedDict = dict
+
+try:
+    # -- SINCE: Python2.7
+    from collections import Counter
+except ImportError:     # pragma: no cover
+    class Counter(dict):
+        '''Dict subclass for counting hashable objects.  Sometimes called a bag
+        or multiset.  Elements are stored as dictionary keys and their counts
+        are stored as dictionary values.
+
+        >>> Counter('zyzygy')
+        Counter({'y': 3, 'z': 2, 'g': 1})
+
+        '''
+
+        def __init__(self, iterable=None, **kwds):
+            '''Create a new, empty Counter object.  And if given, count elements
+            from an input iterable.  Or, initialize the count from another mapping
+            of elements to their counts.
+
+            >>> c = Counter()                           # a new, empty counter
+            >>> c = Counter('gallahad')                 # a new counter from an iterable
+            >>> c = Counter({'a': 4, 'b': 2})           # a new counter from a mapping
+            >>> c = Counter(a=4, b=2)                   # a new counter from keyword args
+
+            '''
+            self.update(iterable, **kwds)
+
+        def __missing__(self, key):
+            return 0
+
+        def most_common(self, n=None):
+            '''List the n most common elements and their counts from the most
+            common to the least.  If n is None, then list all element counts.
+
+            >>> Counter('abracadabra').most_common(3)
+            [('a', 5), ('r', 2), ('b', 2)]
+
+            '''
+            if n is None:
+                return sorted(self.iteritems(), key=itemgetter(1), reverse=True)
+            return nlargest(n, self.iteritems(), key=itemgetter(1))
+
+        def elements(self):
+            '''Iterator over elements repeating each as many times as its count.
+
+            >>> c = Counter('ABCABC')
+            >>> sorted(c.elements())
+            ['A', 'A', 'B', 'B', 'C', 'C']
+
+            If an element's count has been set to zero or is a negative number,
+            elements() will ignore it.
+
+            '''
+            for elem, count in self.iteritems():
+                for _ in repeat(None, count):
+                    yield elem
+
+        # Override dict methods where the meaning changes for Counter objects.
+
+        @classmethod
+        def fromkeys(cls, iterable, v=None):
+            raise NotImplementedError(
+                'Counter.fromkeys() is undefined.  Use Counter(iterable) instead.')
+
+        def update(self, iterable=None, **kwds):
+            '''Like dict.update() but add counts instead of replacing them.
+
+            Source can be an iterable, a dictionary, or another Counter instance.
+
+            >>> c = Counter('which')
+            >>> c.update('witch')           # add elements from another iterable
+            >>> d = Counter('watch')
+            >>> c.update(d)                 # add elements from another counter
+            >>> c['h']                      # four 'h' in which, witch, and watch
+            4
+
+            '''
+            if iterable is not None:
+                if hasattr(iterable, 'iteritems'):
+                    if self:
+                        self_get = self.get
+                        for elem, count in iterable.iteritems():
+                            self[elem] = self_get(elem, 0) + count
+                    else:
+                        dict.update(self, iterable) # fast path when counter is empty
+                else:
+                    self_get = self.get
+                    for elem in iterable:
+                        self[elem] = self_get(elem, 0) + 1
+            if kwds:
+                self.update(kwds)
+
+        def copy(self):
+            'Like dict.copy() but returns a Counter instance instead of a dict.'
+            return Counter(self)
+
+        def __delitem__(self, elem):
+            'Like dict.__delitem__() but does not raise KeyError for missing values.'
+            if elem in self:
+                dict.__delitem__(self, elem)
+
+        def __repr__(self):
+            if not self:
+                return '%s()' % self.__class__.__name__
+            items = ', '.join(map('%r: %r'.__mod__, self.most_common()))
+            return '%s({%s})' % (self.__class__.__name__, items)
+
+        # Multiset-style mathematical operations discussed in:
+        #       Knuth TAOCP Volume II section 4.6.3 exercise 19
+        #       and at http://en.wikipedia.org/wiki/Multiset
+        #
+        # Outputs guaranteed to only include positive counts.
+        #
+        # To strip negative and zero counts, add-in an empty counter:
+        #       c += Counter()
+
+        def __add__(self, other):
+            '''Add counts from two counters.
+
+            >>> Counter('abbb') + Counter('bcc')
+            Counter({'b': 4, 'c': 2, 'a': 1})
+
+
+            '''
+            if not isinstance(other, Counter):
+                return NotImplemented
+            result = Counter()
+            for elem in set(self) | set(other):
+                newcount = self[elem] + other[elem]
+                if newcount > 0:
+                    result[elem] = newcount
+            return result
+
+        def __sub__(self, other):
+            ''' Subtract count, but keep only results with positive counts.
+
+            >>> Counter('abbbc') - Counter('bccd')
+            Counter({'b': 2, 'a': 1})
+
+            '''
+            if not isinstance(other, Counter):
+                return NotImplemented
+            result = Counter()
+            for elem in set(self) | set(other):
+                newcount = self[elem] - other[elem]
+                if newcount > 0:
+                    result[elem] = newcount
+            return result
+
+        def __or__(self, other):
+            '''Union is the maximum of value in either of the input counters.
+
+            >>> Counter('abbb') | Counter('bcc')
+            Counter({'b': 3, 'c': 2, 'a': 1})
+
+            '''
+            if not isinstance(other, Counter):
+                return NotImplemented
+            _max = max
+            result = Counter()
+            for elem in set(self) | set(other):
+                newcount = _max(self[elem], other[elem])
+                if newcount > 0:
+                    result[elem] = newcount
+            return result
+
+        def __and__(self, other):
+            ''' Intersection is the minimum of corresponding counts.
+
+            >>> Counter('abbb') & Counter('bcc')
+            Counter({'b': 1})
+
+            '''
+            if not isinstance(other, Counter):
+                return NotImplemented
+            _min = min
+            result = Counter()
+            if len(self) < len(other):
+                self, other = other, self
+            for elem in ifilter(self.__contains__, other):
+                newcount = _min(self[elem], other[elem])
+                if newcount > 0:
+                    result[elem] = newcount
+            return result
diff --git a/behave/configuration.py b/behave/configuration.py
index a2563df..b91f2e8 100644
--- a/behave/configuration.py
+++ b/behave/configuration.py
@@ -136,6 +136,10 @@ options = [
           help="""Specify name annotation schema for scenario outline
                   (default="{name} -- @{row.id} {examples.name}").""")),
 
+    ((),  # -- CONFIGFILE only
+     dict(dest='css',
+          help="""Specify a different css for HTML formatter""")),
+
 #    (('-g', '--guess'),
 #     dict(action='store_true',
 #          help="Guess best match for ambiguous steps.")),
@@ -503,7 +507,8 @@ class Configuration(object):
         stage=None,
         userdata={},
         # -- SPECIAL:
-        default_format="pretty",   # -- Used when no formatters are configured.
+        default_format="pretty",    # -- Used when no formatters are configured.
+        css=None,
         scenario_outline_annotation_schema=u"{name} -- @{row.id} {examples.name}"
     )
     cmdline_only_options = set("userdata_defines")
@@ -545,6 +550,7 @@ class Configuration(object):
         self.outputs = []
         self.include_re = None
         self.exclude_re = None
+        self.css = None
         self.scenario_outline_annotation_schema = None
         self.steps_dir = "steps"
         self.environment_file = "environment.py"
diff --git a/behave/formatter/_builtins.py b/behave/formatter/_builtins.py
index 8c2e52e..8e9d37c 100644
--- a/behave/formatter/_builtins.py
+++ b/behave/formatter/_builtins.py
@@ -28,6 +28,7 @@ _BUILTIN_FORMATS = [
     ("steps.catalog", "behave.formatter.steps:StepsCatalogFormatter"),
     ("steps.usage",   "behave.formatter.steps:StepsUsageFormatter"),
     ("sphinx.steps",  "behave.formatter.sphinx_steps:SphinxStepsFormatter"),
+    ("html",          "behave.formatter.html:HTMLFormatter"),
 ]
 
 # -----------------------------------------------------------------------------
diff --git a/behave/formatter/behave.css b/behave/formatter/behave.css
new file mode 100644
index 0000000..44f7685
--- /dev/null
+++ b/behave/formatter/behave.css
@@ -0,0 +1,241 @@
+// SOURCE: https://raw.githubusercontent.com/vrutkovs/behave/html_with_coveralls/behave/formatter/report.css
+
+// -- RESULT-STATUS RELATED STYLES:
+.passed {
+}
+
+.failed {
+}
+
+.error {
+}
+
+.skipped {
+}
+
+.undefined {
+}
+
+// -- CONTENT-RELATED STYLES:
+.summary {
+}
+
+.failed_scenarios {
+}
+
+.footer {
+}
+
+// -- ORIGINAL-STARTS-HERE
+body {
+  font-size: 0px;
+  color: white;
+  margin: 0px;
+  padding: 0px;
+}
+
+.behave, td, th {
+  font: normal 11px "Lucida Grande", Helvetica, sans-serif;
+  background: white;
+  color: black;
+}
+.behave #behave-header, td #behave-header, th #behave-header {
+  background: #65c400;
+  color: white;
+  height: 8em;
+}
+.behave #behave-header #expand-collapse p, td #behave-header #expand-collapse p, th #behave-header #expand-collapse p {
+  float: right;
+  margin: 0 0 0 10px;
+}
+.behave .scenario h3, td .scenario h3, th .scenario h3, .background h3 {
+  font-size: 11px;
+  padding: 3px;
+  margin: 0;
+  background: #65c400;
+  color: white;
+  font-weight: bold;
+}
+
+.background h3 {
+  font-size: 1.2em;
+  background: #666;
+}
+
+.behave h1, td h1, th h1 {
+  margin: 0px 10px 0px 10px;
+  padding: 10px;
+  font-family: "Lucida Grande", Helvetica, sans-serif;
+  font-size: 2em;
+  position: absolute;
+}
+.behave h4, td h4, th h4 {
+  margin-bottom: 2px;
+}
+.behave div.feature, td div.feature, th div.feature {
+  padding: 2px;
+  margin: 0px 10px 5px 10px;
+}
+.behave div.examples, td div.examples, th div.examples {
+  padding: 0em 0em 0em 1em;
+}
+.behave .stats, td .stats, th .stats {
+  margin: 2em;
+}
+.behave .summary ul.features li, td .summary ul.features li, th .summary ul.features li {
+  display: inline;
+}
+.behave .step_name, td .step_name, th .step_name {
+  float: left;
+}
+.behave .step_file, td .step_file, th .step_file {
+  text-align: right;
+  color: #999999;
+}
+.behave .step_file a, td .step_file a, th .step_file a {
+  color: #999999;
+}
+.behave .scenario_file, td .scenario_file, th .scenario_file {
+  float: right;
+  color: #999999;
+}
+.behave .tag, td .tag, th .tag {
+  font-weight: bold;
+  color: #246ac1;
+}
+.behave .backtrace, td .backtrace, th .backtrace {
+  margin-top: 0;
+  margin-bottom: 0;
+  margin-left: 1em;
+  color: black;
+}
+.behave a, td a, th a {
+  text-decoration: none;
+  color: #be5c00;
+}
+.behave a:hover, td a:hover, th a:hover {
+  text-decoration: underline;
+}
+.behave a:visited, td a:visited, th a:visited {
+  font-weight: normal;
+}
+.behave a div.examples, td a div.examples, th a div.examples {
+  margin: 5px 0px 5px 15px;
+  color: black;
+}
+.behave .outline table, td .outline table, th .outline table {
+  margin: 0px 0px 5px 10px;
+}
+.behave table, td table, th table {
+  border-collapse: collapse;
+}
+.behave table td, td table td, th table td {
+  padding: 3px 3px 3px 5px;
+}
+.behave table td.failed, .behave table td.passed, .behave table td.skipped, .behave table td.pending, .behave table td.undefined, td table td.failed, td table td.passed, td table td.skipped, td table td.pending
+  padding-left: 18px;
+  padding-right: 10px;
+}
+.behave table td.failed, td table td.failed, th table td.failed {
+  border-left: 5px solid #c20000;
+  border-bottom: 1px solid #c20000;
+  background: #fffbd3;
+  color: #c20000;
+}
+.behave table td.passed, td table td.passed, th table td.passed {
+  border-left: 5px solid #65c400;
+  border-bottom: 1px solid #65c400;
+  background: #dbffb4;
+  color: #3d7700;
+}
+.behave table td.skipped, td table td.skipped, th table td.skipped {
+  border-left: 5px solid aqua;
+  border-bottom: 1px solid aqua;
+  background: #e0ffff;
+  color: #001111;
+}
+.behave table td.pending, td table td.pending, th table td.pending {
+  border-left: 5px solid #faf834;
+  border-bottom: 1px solid #faf834;
+  background: #fcfb98;
+  color: #131313;
+}
+.behave table td.undefined, td table td.undefined, th table td.undefined {
+  border-left: 5px solid #faf834;
+  border-bottom: 1px solid #faf834;
+  background: #fcfb98;
+  color: #131313;
+}
+.behave table td.message, td table td.message, th table td.message {
+  border-left: 5px solid aqua;
+  border-bottom: 1px solid aqua;
+  background: #e0ffff;
+  color: #001111;
+}
+.behave ol, td ol, th ol {
+  list-style: none;
+  margin: 0px;
+  padding: 0px;
+}
+.behave ol li.step, td ol li.step, th ol li.step {
+  padding: 3px 3px 3px 18px;
+  margin: 5px 0px 5px 5px;
+}
+.behave ol li, td ol li, th ol li {
+  margin: 0em 0em 0em 1em;
+  padding: 0em 0em 0em 0.2em;
+}
+.behave ol li span.param, td ol li span.param, th ol li span.param {
+  font-weight: bold;
+}
+.behave ol li.failed, td ol li.failed, th ol li.failed {
+  border-left: 5px solid #c20000;
+  border-bottom: 1px solid #c20000;
+  background: #fffbd3;
+  color: #c20000;
+}
+.behave ol li.passed, td ol li.passed, th ol li.passed {
+  border-left: 5px solid #65c400;
+  border-bottom: 1px solid #65c400;
+  background: #dbffb4;
+  color: #3d7700;
+}
+.behave ol li.skipped, td ol li.skipped, th ol li.skipped {
+  border-left: 5px solid aqua;
+  border-bottom: 1px solid aqua;
+  background: #e0ffff;
+  color: #001111;
+}
+.behave ol li.pending, td ol li.pending, th ol li.pending {
+  border-left: 5px solid #faf834;
+  border-bottom: 1px solid #faf834;
+  background: #fcfb98;
+  color: #131313;
+}
+.behave ol li.undefined, td ol li.undefined, th ol li.undefined {
+  border-left: 5px solid #faf834;
+  border-bottom: 1px solid #faf834;
+  background: #fcfb98;
+  color: #131313;
+}
+.behave ol li.message, td ol li.message, th ol li.message {
+  border-left: 5px solid aqua;
+  border-bottom: 1px solid aqua;
+  background: #e0ffff;
+  color: #001111;
+  margin-left: 10px;
+}
+.behave #summary, td #summary, th #summary {
+  margin: 0px;
+  padding: 5px 10px;
+  text-align: right;
+  top: 0px;
+  right: 0px;
+  float: right;
+}
+.behave #summary p, td #summary p, th #summary p {
+  margin: 0 0 0 2px;
+}
+.behave #summary #totals, td #summary #totals, th #summary #totals {
+  font-size: 1.2em;
+}
diff --git a/behave/formatter/html.py b/behave/formatter/html.py
new file mode 100644
index 0000000..874696e
--- /dev/null
+++ b/behave/formatter/html.py
@@ -0,0 +1,496 @@
+# -*- coding: utf-8 -*-
+"""
+HTML formatter for behave.
+Writes a single-page HTML file for test run with all features/scenarios.
+
+
+IMPROVEMENTS:
+  + Avoid to use lxml.etree, use xml.etree.ElementTree instead (bundled w/ Python)
+  + Add pretty_print functionality to provide lxml goodie.
+  + Stylesheet should be (easily) replacable
+  + Simplify collapsable-section usage:
+    => Only JavaScript-call: onclick = Collapsible_toggle('xxx')
+    => Avoid code duplications, make HTML more readable
+  + Expand All / Collapse All: Use <a> instead of <span> element
+    => Make active logic (actions) more visible
+  * Support external stylesheet ?!?
+  * Introduce (Html)Page class to simplify extension and replacements
+  * Separate business layer (HtmlFormatter) from technology layer (Page).
+  * Correct Python2 constructs: map()/reduce()
+  * end() or stream.close() handling is missing
+  * steps: text, table parts are no so easily detectable
+  * CSS: stylesheet should contain logical "style" classes.
+    => AVOID using combination of style attributes where style is better.
+
+TODO:
+  * Embedding only works with one part ?!?
+  * Even empty embed elements are contained ?!?
+"""
+
+from behave.formatter.base import Formatter
+from behave.compat.collections import Counter
+# XXX-JE-OLD: import lxml.etree as ET
+import xml.etree.ElementTree as ET
+import base64
+# XXX-JE-NOT-USED: import os.path
+
+
+def _valid_XML_char_ordinal(i):
+    return (  # conditions ordered by presumed frequency
+        0x20 <= i <= 0xD7FF
+        or i in (0x9, 0xA, 0xD)
+        or 0xE000 <= i <= 0xFFFD
+        or 0x10000 <= i <= 0x10FFFF
+    )
+
+# XXX-JE-FIRST-IDEA:
+# def html_prettify(elem):
+#     """Return a pretty-printed XML string for the Element."""
+#     rough_string = ET.tostring(elem, "utf-8") # XXX, method="html")
+#     reparsed = minidom.parseString(rough_string)
+#     return reparsed.toprettyxml(indent="  ")
+
+def ET_tostring(elem, pretty_print=False):
+    """Render an HTML element(tree) and optionally pretty-print it."""
+
+    text = ET.tostring(elem, "utf-8")   # XXX, method="html")
+    if pretty_print:
+        # -- RECIPE: For pretty-printing w/ xml.etree.ElementTree.
+        # SEE: http://pymotw.com/2/xml/etree/ElementTree/create.html
+        from xml.dom import minidom
+        import re
+        declaration_len = len(minidom.Document().toxml())
+        reparsed = minidom.parseString(text)
+        text = reparsed.toprettyxml(indent="  ")[declaration_len:]
+        text_re = re.compile(r'>\n\s+([^<>\s].*?)\n\s+</', re.DOTALL)
+        text = text_re.sub(r'>\g<1></', text)
+    return text
+
+class JavascriptLibrary(object):
+    collapsible = """
+function Collapsible_toggle(id)
+{
+    var elem = document.getElementById(id);
+    elem.style.display = (elem.style.display == 'none' ? 'block' : 'none');
+    return false;
+}
+
+function Collapsible_expandAll(className)
+{
+    var elems = document.getElementsByClassName(className);
+    for (var i=0; i < elems.length; i++) {
+        elems[i].style.display = 'block';
+    }
+}
+
+function Collapsible_collapseAll(className)
+{
+    var elems = document.getElementsByClassName(className);
+    for (var i=0; i < elems.length; i++) {
+        elems[i].style.display = 'none';
+    }
+}
+
+function Collapsible_expandAllFailed()
+{
+    var elems = document.getElementsByClassName('failed');
+    for (var i=0; i < elems.length; i++) {
+        var elem = elems[i];
+        if (elem.nodeName == 'H3'){
+            elem.parentElement.getElementsByTagName('ol')[0].style.display = 'block';
+        }
+    }
+}
+"""
+
+
+class BasicTheme(object):
+    stylesheet_text = """
+body{font-size:0;color:#fff;margin:0;
+padding:0}.behave,td,th{font:400 11px "Lucida Grande",Helvetica,sans-serif;
+background:#fff;color:#000}.behave #behave-header,td #behave-header,
+th #behave-header{background:#65c400;color:#fff;height:8em}.behave
+#behave-header #expand-collapse p,td #behave-header #expand-collapse
+p,th #behave-header #expand-collapse p{float:right;margin:0 0 0 10px}
+.background h3,.behave .scenario h3,td .scenario h3,th .scenario h3{
+font-size:11px;padding:3px;margin:0;background:#65c400;color:#fff;
+font-weight:700}.background h3{font-size:1.2em;background:#666}.behave
+h1,td h1,th h1{margin:0 10px;padding:10px;font-family:'Lucida Grande',
+Helvetica,sans-serif;font-size:2em;position:absolute}.behave h4,td h4,
+th h4{margin-bottom:2px}.behave div.feature,td div.feature,th div.feature
+{padding:2px;margin:0 10px 5px}.behave div.examples,td div.examples,th
+div.examples{padding:0 0 0 1em}.behave .stats,td .stats,th .stats{margin:2em}
+.behave .summary ul.features li,td .summary ul.features li,th .summary
+ul.features li{display:inline}.behave .step_name,td .step_name,th .step_name
+{float:left}.behave .step_file,td .step_file,th .step_file{text-align:right;
+color:#999}.behave .step_file a,td .step_file a,th .step_file a{color:#999}.behave
+.scenario_file,td .scenario_file,th .scenario_file{float:right;color:#999}.behave
+.tag,td .tag,th .tag{font-weight:700;color:#246ac1}.behave .backtrace,td
+.backtrace,th .backtrace{margin-top:0;margin-bottom:0;margin-left:1em;color:#000}
+.behave a,td a,th a{text-decoration:none;color:#be5c00}.behave a:hover,
+td a:hover,th a:hover{text-decoration:underline}.behave a:visited,td a:visited,
+th a:visited{font-weight:400}.behave a div.examples,td a div.examples,
+th a div.examples{margin:5px 0 5px 15px;color:#000}.behave .outline table,
+td .outline table,th .outline table{margin:0 0 5px 10px}.behave table,
+td table,th table{border-collapse:collapse}.behave table td,td table td,
+th table td{padding:3px 3px 3px 5px}.behave table td.failed,td table td.failed,
+th table td.failed{border-left:5px solid #c20000;border-bottom:1px solid
+#c20000;background:#fffbd3;color:#c20000}.behave table td.passed,td table
+td.passed,th table td.passed{border-left:5px solid #65c400;border-bottom:1px
+solid #65c400;background:#dbffb4;color:#3d7700}.behave table td.skipped,td
+table td.skipped,th table td.skipped{border-left:5px solid #0ff;border-bottom:1px
+solid #0ff;background:#e0ffff;color:#011}.behave table td.pending,.behave table
+td.undefined,td table td.pending,td table td.undefined,th table td.pending,th table
+td.undefined{border-left:5px solid #faf834;border-bottom:1px solid #faf834;
+background:#fcfb98;color:#131313}.behave table td.message,td table td.message,th
+table td.message{border-left:5px solid #0ff;border-bottom:1px solid #0ff;
+background:#e0ffff;color:#011}.behave ol,td ol,th ol{list-style:none;
+margin:0;padding:0}.behave ol li.step,td ol li.step,th ol li.step{
+padding:3px 3px 3px 18px;margin:5px 0 5px 5px}.behave ol li,td ol li,th
+ol li{margin:0 0 0 1em;padding:0 0 0 .2em}.behave ol li span.param,td
+ol li span.param,th ol li span.param{font-weight:700}.behave ol li.failed,td
+ol li.failed,th ol li.failed{border-left:5px solid #c20000;border-bottom:1px
+solid #c20000;background:#fffbd3;color:#c20000}.behave ol li.passed,td ol
+li.passed,th ol li.passed{border-left:5px solid #65c400;border-bottom:1px
+solid #65c400;background:#dbffb4;color:#3d7700}.behave ol li.skipped,td ol
+li.skipped,th ol li.skipped{border-left:5px solid #0ff;border-bottom:1px
+solid #0ff;background:#e0ffff;color:#011}.behave ol li.pending,.behave ol
+li.undefined,td ol li.pending,td ol li.undefined,th ol li.pending,th ol
+li.undefined{border-left:5px solid #faf834;border-bottom:1px solid
+#faf834;background:#fcfb98;color:#131313}.behave ol li.message,td ol
+li.message,th ol li.message{border-left:5px solid #0ff;border-bottom:1px
+solid #0ff;background:#e0ffff;color:#011;margin-left:10px}.behave #summary,td
+#summary,th #summary{margin:0;padding:5px 10px;text-align:right;top:0;
+right:0;float:right}.behave #summary p,td #summary p,th #summary
+p{margin:0 0 0 2px}.behave #summary #totals,td #summary #totals,th
+#summary #totals{font-size:1.2em} h3.failed,#behave-header.failed{background:
+#c40d0d !important} h3.undefined,#behave-header.undefined{background:#faf834
+ !important; color:#000 !important} #behave-header.failed a{color:#fff} pre {
+ white-space: pre-wrap}
+"""
+
+
+class Page(object):
+    """
+    Provides a HTML page construct (as technological layer).
+    XXX
+    """
+    theme = BasicTheme
+
+    def __init__(self, title=None):
+        pass
+
+
+class HTMLFormatter(Formatter):
+    """Provides a single-page HTML formatter
+    that writes the result of a  test run.
+    """
+    name = 'html'
+    description = 'Very basic HTML formatter'
+    title = u"Behave Test Report"
+
+    def __init__(self, stream, config):
+        super(HTMLFormatter, self).__init__(stream, config)
+
+        # -- XXX-JE-PREPARED-BUT-DISABLED:
+        # XXX Seldom changed value.
+        # XXX Should only be in configuration-file in own section "behave.formatter.html" ?!?
+        # XXX Config support must be provided.
+        # XXX REASON: Don't clutter behave config-space w/ formatter/plugin related config data.
+        # self.css = self.default_css
+        # if config.css is not None:
+        #    self.css = config.css
+        self.html = ET.Element('html')
+        head = ET.SubElement(self.html, 'head')
+        ET.SubElement(head, 'title').text = self.title
+        ET.SubElement(head, 'meta', {'content': 'text/html;charset=utf-8'})
+        style = ET.SubElement(head, 'style', type=u"text/css")
+        style.append(ET.Comment(Page.theme.stylesheet_text))
+        script = ET.SubElement(head, 'script', type=u"text/javascript")
+        script_text = ET.Comment(JavascriptLibrary.collapsible)
+        script.append(script_text)
+
+        self.stream = self.open()
+        body = ET.SubElement(self.html, 'body')
+        self.suite = ET.SubElement(body, 'div', {'class': 'behave'})
+
+        #Summary
+        self.header = ET.SubElement(self.suite, 'div', id='behave-header')
+        label = ET.SubElement(self.header, 'div', id='label')
+        ET.SubElement(label, 'h1').text = self.title
+
+        summary = ET.SubElement(self.header, 'div', id='summary')
+
+        totals = ET.SubElement(summary, 'p', id='totals')
+
+        self.current_feature_totals = ET.SubElement(totals, 'p', id='feature_totals')
+        self.scenario_totals = ET.SubElement(totals, 'p', id='scenario_totals')
+        self.step_totals = ET.SubElement(totals, 'p', id='step_totals')
+        self.duration = ET.SubElement(summary, 'p', id='duration')
+
+        # -- PART: Expand/Collapse All
+        expand_collapse = ET.SubElement(summary, 'div', id='expand-collapse')
+        expander = ET.SubElement(expand_collapse, 'a', id='expander', href="#")
+        expander.set('onclick', "Collapsible_expandAll('scenario_steps')")
+        expander.text = u'Expand All'
+        cea_spacer = ET.SubElement(expand_collapse, 'span')
+        cea_spacer.text = u" | "
+        collapser = ET.SubElement(expand_collapse, 'a', id='collapser', href="#")
+        collapser.set('onclick', "Collapsible_collapseAll('scenario_steps')")
+        collapser.text = u'Collapse All'
+        cea_spacer = ET.SubElement(expand_collapse, 'span')
+        cea_spacer.text = u" | "
+        expander = ET.SubElement(expand_collapse, 'a', id='failed_expander', href="#")
+        expander.set('onclick', "Collapsible_expandAllFailed()")
+        expander.text = u'Expand All Failed'
+
+
+        self.embed_id = 0
+        self.embed_in_this_step = None
+        self.embed_data = None
+        self.embed_mime_type = None
+        self.scenario_id = 0
+
+    def feature(self, feature):
+        if not hasattr(self, "all_features"):
+            self.all_features = []
+        self.all_features.append(feature)
+
+        self.current_feature = ET.SubElement(self.suite, 'div', {'class': 'feature'})
+        if feature.tags:
+            tags_element = ET.SubElement(self.current_feature, 'span', {'class': 'tag'})
+            tags_element.text = u'@' + ', @'.join(feature.tags)
+        h2 = ET.SubElement(self.current_feature, 'h2')
+        feature_element = ET.SubElement(h2, 'span', {'class': 'val'})
+        feature_element.text = u'%s: %s' % (feature.keyword, feature.name)
+        if feature.description:
+            description_element = ET.SubElement(self.current_feature, 'pre', {'class': 'message'})
+            description_element.text = '\n'.join(feature.description)
+
+    def background(self, background):
+        self.current_background = ET.SubElement(self.suite, 'div', {'class': 'background'})
+
+        h3 = ET.SubElement(self.current_background, 'h3')
+        ET.SubElement(h3, 'span', {'class': 'val'}).text = \
+            u'%s: %s' % (background.keyword, background.name)
+
+        self.steps = ET.SubElement(self.current_background, 'ol')
+
+    def scenario(self, scenario):
+        if scenario.feature not in self.all_features:
+            self.all_features.append(scenario.feature)
+        self.scenario_el = ET.SubElement(self.suite, 'div', {'class': 'scenario'})
+
+        scenario_file = ET.SubElement(self.scenario_el, 'span', {'class': 'scenario_file'})
+        scenario_file.text = "%s:%s" % (scenario.location.filename, scenario.location.line)
+
+        if scenario.tags:
+            tags = ET.SubElement(self.scenario_el, 'span', {'class': 'tag'})
+            tags.text = u'@' + ', @'.join(scenario.tags)
+
+        self.scenario_name = ET.SubElement(self.scenario_el, 'h3')
+        span = ET.SubElement(self.scenario_name, 'span', {'class': 'val'})
+        span.text = u'%s: %s' % (scenario.keyword, scenario.name)
+
+        if scenario.description:
+            description_element = ET.SubElement(self.scenario_el, 'pre', {'class': 'message'})
+            description_element.text = '\n'.join(scenario.description)
+
+        self.steps = ET.SubElement(self.scenario_el, 'ol',
+                                   {'class': 'scenario_steps',
+                                    'id': 'scenario_%s' % self.scenario_id})
+
+        self.scenario_name.set('onclick',
+                "Collapsible_toggle('scenario_%s')" % self.scenario_id)
+        self.scenario_id += 1
+
+    def scenario_outline(self, outline):
+        self.scenario(self, outline)
+        self.scenario_el.set('class', 'scenario outline')
+
+    def match(self, match):
+        self.arguments = match.arguments
+        if match.location:
+            self.location = "%s:%s" % (match.location.filename, match.location.line)
+        else:
+            self.location = "<unknown>"
+
+    def step(self, step):
+        self.arguments = None
+        self.embed_in_this_step = None
+        self.last_step = step
+
+    def result(self, result):
+        self.last_step = result
+        step = ET.SubElement(self.steps, 'li', {'class': 'step %s' % result.status})
+        step_name = ET.SubElement(step, 'div', {'class': 'step_name'})
+
+        keyword = ET.SubElement(step_name, 'span', {'class': 'keyword'})
+        keyword.text = result.keyword + u' '
+
+        step_text = ET.SubElement(step_name, 'span', {'class': 'step val'})
+        if self.arguments:
+            text_start = 0
+            for argument in self.arguments:
+                step_part = ET.SubElement(step_text, 'span')
+                step_part.text = result.name[text_start:argument.start]
+                ET.SubElement(step_text, 'b').text = str(argument.value)
+                text_start = argument.end
+            step_part = ET.SubElement(step_text, 'span')
+            step_part.text = result.name[self.arguments[-1].end:]
+        else:
+            step_text.text = result.name
+
+        step_file = ET.SubElement(step, 'div', {'class': 'step_file'})
+        ET.SubElement(step_file, 'span').text = self.location
+
+        self.last_step_embed_span = ET.SubElement(step, 'span')
+        self.last_step_embed_span.set('class', 'embed')
+
+        if result.text:
+            message = ET.SubElement(step, 'div', {'class': 'message'})
+            pre = ET.SubElement(message, 'pre')
+            pre.text = result.text
+
+        if result.table:
+            table = ET.SubElement(step, 'table')
+            tr = ET.SubElement(table, 'tr')
+            for heading in result.table.headings:
+                ET.SubElement(tr, 'th').text = heading
+
+            for row in result.table.rows:
+                tr = ET.SubElement(table, 'tr')
+                for cell in row.cells:
+                    ET.SubElement(tr, 'td').text = cell
+
+        if result.error_message:
+            self.embed_id += 1
+            link = ET.SubElement(step, 'a', {'class': 'message'})
+            link.set("onclick",
+                    "Collapsible_toggle('embed_%s')" % self.embed_id)
+            link.text = u'Error message'
+
+            embed = ET.SubElement(step, 'pre',
+                                  {'id': "embed_%s" % self.embed_id,
+                                   'style': 'display: none'})
+            cleaned_error_message = ''.join(
+                c for c in result.error_message if _valid_XML_char_ordinal(ord(c))
+            )
+            embed.text = cleaned_error_message
+            embed.tail = u'    '
+
+        if result.status == 'failed':
+            self.scenario_name.set('class', 'failed')
+            self.header.set('class', 'failed')
+
+        if result.status == 'undefined':
+            self.scenario_name.set('class', 'undefined')
+            self.header.set('class', 'undefined')
+
+        if hasattr(self, 'embed_in_this_step') and self.embed_in_this_step:
+            self._doEmbed(self.last_step_embed_span,
+                          self.embed_mime_type,
+                          self.embed_data,
+                          self.embed_caption)
+            self.embed_in_this_step = None
+
+    def _doEmbed(self, span, mime_type, data, caption):
+        self.embed_id += 1
+
+        link = ET.SubElement(span, 'a')
+        link.set("onclick", "Collapsible_toggle('embed_%s')" % self.embed_id)
+
+        if 'video/' in mime_type:
+            if not caption:
+                caption = u'Video'
+            link.text = unicode(caption)
+
+            embed = ET.SubElement(span, 'video',
+                                  {'id': 'embed_%s' % self.embed_id,
+                                   'style': 'display: none',
+                                   'width': '320',
+                                   'controls': ''})
+            embed.tail = u'    '
+            ET.SubElement(embed, 'source',{
+                          'src': u'data:%s;base64,%s' % (mime_type, base64.b64encode(data)),
+                          'type': '%s; codecs="vp8 vorbis"' % mime_type})
+
+        if 'image/' in mime_type:
+            if not caption:
+                caption = u'Screenshot'
+            link.text = unicode(caption)
+
+            embed = ET.SubElement(span, 'img', {
+                                  'id': 'embed_%s' % self.embed_id,
+                                  'style': 'display: none',
+                                  'src': u'data:%s;base64,%s' % (
+                                      mime_type, base64.b64encode(data))})
+            embed.tail = u'    '
+
+        if 'text/' in mime_type:
+            if not caption:
+                caption = u'Data'
+            link.text = unicode(caption)
+
+            cleaned_data = ''.join(
+                c for c in data if _valid_XML_char_ordinal(ord(c))
+            )
+
+            embed = ET.SubElement(span, 'pre',
+                                  {'id': "embed_%s" % self.embed_id,
+                                   'style': 'display: none'})
+            embed.text = cleaned_data
+            embed.tail = u'    '
+
+    def embedding(self, mime_type, data, caption=None):
+        if self.last_step.status == 'untested':
+            # Embed called during step execution
+            self.embed_in_this_step = True
+            self.embed_mime_type = mime_type
+            self.embed_data = data
+            self.embed_caption = caption
+        else:
+            # Embed called in after_*
+            self._doEmbed(self.last_step_embed_span, mime_type, data, caption)
+
+    def close(self):
+        if not hasattr(self, "all_features"):
+            self.all_features = []
+        self.duration.text =\
+            u"Finished in %0.1f seconds" %\
+            sum([x.duration for x in self.all_features])
+
+        # Filling in summary details
+        result = []
+        statuses = [x.status for x in self.all_features]
+        status_counter = Counter(statuses)
+        for k in status_counter:
+            result.append('%s: %s' % (k, status_counter[k]))
+        self.current_feature_totals.text = u'Features: %s' % ', '.join(result)
+
+        result = []
+        scenarios_list = [x.scenarios for x in self.all_features]
+        scenarios = []
+        if len(scenarios_list) > 0:
+            scenarios = [x for subl in scenarios_list for x in subl]
+        statuses = [x.status for x in scenarios]
+        status_counter = Counter(statuses)
+        for k in status_counter:
+            result.append('%s: %s' % (k, status_counter[k]))
+        self.scenario_totals.text = u'Scenarios: %s' % ', '.join(result)
+
+        result = []
+        step_list = [x.steps for x in scenarios]
+        steps = []
+        if step_list:
+            steps = [x for subl in step_list for x in subl]
+        statuses = [x.status for x in steps]
+        status_counter = Counter(statuses)
+        for k in status_counter:
+            result.append('%s: %s' % (k, status_counter[k]))
+        self.step_totals.text = u'Steps: %s' % ', '.join(result)
+
+        # Sending the report to stream
+        if len(self.all_features) > 0:
+            self.stream.write(u"<!DOCTYPE HTML>\n")
+            self.stream.write(ET_tostring(self.html, pretty_print=True))
diff --git a/behave/runner.py b/behave/runner.py
index 8e457c6..56ea77f 100644
--- a/behave/runner.py
+++ b/behave/runner.py
@@ -254,6 +254,11 @@ class Context(object):
                 return True
         return False
 
+    def embed(self, mime_type, data, caption=None):
+        for formatter in self._runner.formatters:
+            if hasattr(formatter, 'embedding'):
+                formatter.embedding(mime_type, data, caption)
+
     def execute_steps(self, steps_text):
         '''The steps identified in the "steps" text string will be parsed and
         executed in turn just as though they were defined in a feature file.
diff --git a/features/formatter.help.feature b/features/formatter.help.feature
index 48f1a02..77de2fe 100644
--- a/features/formatter.help.feature
+++ b/features/formatter.help.feature
@@ -11,6 +11,7 @@ Feature: Help Formatter
     And the command output should contain:
       """
       Available formatters:
+        html           Very basic HTML formatter
         json           JSON dump of test run
         json.pretty    JSON dump of test run (human readable)
         null           Provides formatter that does not output anything.
diff --git a/features/formatter.html.feature b/features/formatter.html.feature
new file mode 100644
index 0000000..9b1bd95
--- /dev/null
+++ b/features/formatter.html.feature
@@ -0,0 +1,749 @@
+@sequential
+Feature: HTML Formatter
+
+    In order to export behave results
+    As a tester
+    I want that behave generates test run data in HTML format.
+
+
+    @setup
+    Scenario: Feature Setup
+        Given a new working directory
+        And a file named "features/steps/steps.py" with:
+            """
+            from behave import step
+
+            @step('a step passes')
+            def step_passes(context):
+                pass
+
+            @step('a step fails')
+            def step_fails(context):
+                assert False, "XFAIL-STEP"
+
+            @step('a step with parameter "{param:w}" passes')
+            def step_with_one_param_passes(context, param):
+                pass
+
+            @step('a step with parameter "{param1:w}" and parameter "{param2:w}" passes')
+            def step_with_two_params_passes(context, param1, param2):
+                pass
+            """
+
+    Scenario: Use HTML formatter on feature without scenarios
+        Given a file named "features/feature_without_scenarios.feature" with:
+            """
+            Feature: Simple, empty Feature
+            """
+        When I run "behave -f html features/feature_without_scenarios.feature"
+        Then it should pass with:
+            """
+            0 features passed, 0 failed, 1 skipped
+            0 scenarios passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="feature">
+              <h2>
+                <span class="val">Feature: Simple, empty Feature</span>
+              </h2>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on feature with description
+        Given a file named "features/feature_with_description.feature" with:
+            """
+            Feature: Simple feature with description
+
+                First feature description line.
+                Second feature description line.
+
+                Third feature description line (following an empty line).
+            """
+        When I run "behave -f html features/feature_with_description.feature"
+        Then it should pass with:
+            """
+            0 features passed, 0 failed, 1 skipped
+            0 scenarios passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="feature">
+              <h2>
+                <span class="val">Feature: Simple feature with description</span>
+              </h2>
+              <pre class="message">First feature description line.
+              Second feature description line.
+              Third feature description line (following an empty line).</pre>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on feature with tags
+        Given a file named "features/feature_with_tags.feature" with:
+            """
+            @foo @bar
+            Feature: Simple feature with tags
+            """
+        When I run "behave -f html features/feature_with_tags.feature"
+        Then it should pass with:
+            """
+            0 features passed, 0 failed, 1 skipped
+            0 scenarios passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="feature">
+              <span class="tag">@foo, @bar</span>
+              <h2>
+                <span class="val">Feature: Simple feature with tags</span>
+              </h2>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on feature with one empty scenario
+        Given a file named "features/feature_one_empty_scenario.feature" with:
+            """
+            Feature:
+              Scenario: Simple scenario without steps
+            """
+        When I run "behave -f html features/feature_one_empty_scenario.feature"
+        Then it should pass with:
+            """
+            1 feature passed, 0 failed, 0 skipped
+            1 scenario passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="feature">
+              <h2>
+                <span class="val">Feature: </span>
+              </h2>
+            </div>
+            <div class="scenario">
+              <span class="scenario_file">features/feature_one_empty_scenario.feature:2</span>
+              <h3 onclick="Collapsible_toggle('scenario_0')">
+                <span class="val">Scenario: Simple scenario without steps</span>
+              </h3>
+              <ol class="scenario_steps" id="scenario_0"/>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on feature with one empty scenario and description
+        Given a file named "features/feature_one_empty_scenario_with_description.feature" with:
+            """
+            Feature:
+              Scenario: Simple scenario with description but without steps
+                First scenario description line.
+                Second scenario description line.
+
+                Third scenario description line (after an empty line).
+            """
+        When I run "behave -f html features/feature_one_empty_scenario_with_description.feature"
+        Then it should pass with:
+            """
+            1 feature passed, 0 failed, 0 skipped
+            1 scenario passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="feature">
+              <h2>
+                <span class="val">Feature: </span>
+              </h2>
+            </div>
+            <div class="scenario">
+              <span class="scenario_file">features/feature_one_empty_scenario_with_description.feature:2</span>
+              <h3 onclick="Collapsible_toggle('scenario_0')">
+                <span class="val">Scenario: Simple scenario with description but without steps</span>
+              </h3>
+              <pre class="message">First scenario description line.
+              Second scenario description line.
+              Third scenario description line (after an empty line).</pre>
+              <ol class="scenario_steps" id="scenario_0"/>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on feature with one empty scenario and tags
+        Given a file named "features/feature_one_empty_scenario_with_tags.feature" with:
+            """
+            Feature:
+              @foo @bar
+              Scenario: Simple scenario with tags but without steps
+            """
+        When I run "behave -f html features/feature_one_empty_scenario_with_tags.feature"
+        Then it should pass with:
+            """
+            1 feature passed, 0 failed, 0 skipped
+            1 scenario passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="feature">
+              <h2>
+                <span class="val">Feature: </span>
+              </h2>
+            </div>
+            <div class="scenario">
+              <span class="scenario_file">features/feature_one_empty_scenario_with_tags.feature:3</span>
+              <span class="tag">@foo, @bar</span>
+              <h3 onclick="Collapsible_toggle('scenario_0')">
+                <span class="val">Scenario: Simple scenario with tags but without steps</span>
+              </h3>
+              <ol class="scenario_steps" id="scenario_0"/>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on feature with one passing scenario
+        Given a file named "features/feature_one_passing_scenario.feature" with:
+            """
+            Feature:
+              Scenario: Simple scenario with passing steps
+                  Given a step passes
+                  When a step passes
+                  Then a step passes
+                  And a step passes
+                  But a step passes
+            """
+        When I run "behave -f html features/feature_one_passing_scenario.feature"
+        Then it should pass with:
+            """
+            1 feature passed, 0 failed, 0 skipped
+            1 scenario passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="feature">
+              <h2>
+                <span class="val">Feature: </span>
+              </h2>
+            </div>
+            <div class="scenario">
+              <span class="scenario_file">features/feature_one_passing_scenario.feature:2</span>
+              <h3 onclick="Collapsible_toggle('scenario_0')">
+                <span class="val">Scenario: Simple scenario with passing steps</span>
+              </h3>
+              <ol class="scenario_steps" id="scenario_0">
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Given </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">When </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Then </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">And </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">But </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+              </ol>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on feature with one failing scenario
+        Given a file named "features/feature_one_failing_scenario.feature" with:
+            """
+            Feature:
+              Scenario: Simple scenario with failing step
+                  Given a step passes
+                  When a step passes
+                  Then a step passes
+                  And a step passes
+                  But a step fails
+            """
+        When I run "behave -f html features/feature_one_failing_scenario.feature"
+        Then it should fail with:
+            """
+            0 features passed, 1 failed, 0 skipped
+            0 scenarios passed, 1 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="feature">
+              <h2>
+                <span class="val">Feature: </span>
+              </h2>
+            </div>
+            <div class="scenario">
+              <span class="scenario_file">features/feature_one_failing_scenario.feature:2</span>
+              <h3 class="failed" onclick="Collapsible_toggle('scenario_0')">
+                <span class="val">Scenario: Simple scenario with failing step</span>
+              </h3>
+              <ol class="scenario_steps" id="scenario_0">
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Given </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">When </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Then </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">And </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step failed">
+                  <div class="step_name">
+                    <span class="keyword">But </span>
+                    <span class="step val">a step fails</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:7</span>
+                  </div>
+                  <span class="embed"/>
+                  <a class="message" onclick="Collapsible_toggle('embed_1')">Error message</a>
+                  <pre id="embed_1" style="display: none">Assertion Failed: XFAIL-STEP</pre>
+
+                </li>
+              </ol>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on feature with one scenario with skipped steps
+        Given a file named "features/feature_one_failing_scenario_with_skipped_steps.feature" with:
+            """
+            Feature:
+              Scenario: Simple scenario with failing and skipped steps
+                  Given a step passes
+                  When a step fails
+                  Then a step passes
+                  And a step passes
+                  But a step passes
+            """
+        When I run "behave -f html features/feature_one_failing_scenario_with_skipped_steps.feature"
+        Then it should fail with:
+            """
+            0 features passed, 1 failed, 0 skipped
+            0 scenarios passed, 1 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="feature">
+              <h2>
+                <span class="val">Feature: </span>
+              </h2>
+            </div>
+            <div class="scenario">
+              <span class="scenario_file">features/feature_one_failing_scenario_with_skipped_steps.feature:2</span>
+              <h3 class="failed" onclick="Collapsible_toggle('scenario_0')">
+                <span class="val">Scenario: Simple scenario with failing and skipped steps</span>
+              </h3>
+              <ol class="scenario_steps" id="scenario_0">
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Given </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step failed">
+                  <div class="step_name">
+                    <span class="keyword">When </span>
+                    <span class="step val">a step fails</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:7</span>
+                  </div>
+                  <span class="embed"/>
+                  <a class="message" onclick="Collapsible_toggle('embed_1')">Error message</a>
+                  <pre id="embed_1" style="display: none">Assertion Failed: XFAIL-STEP</pre>
+                </li>
+              </ol>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on feature with three scenarios
+        Given a file named "features/feature_three_scenarios.feature" with:
+            """
+            Feature: Many Scenarios
+              Scenario: Passing
+                  Given a step passes
+                  Then a step passes
+
+              Scenario: Failing
+                  Given a step passes
+                  Then a step fails
+
+              Scenario: Failing with skipped steps
+                  Given a step passes
+                  When a step fails
+                  Then a step passes
+            """
+        When I run "behave -f html features/feature_three_scenarios.feature"
+        Then it should fail with:
+            """
+            0 features passed, 1 failed, 0 skipped
+            1 scenario passed, 2 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="scenario">
+              <span class="scenario_file">features/feature_three_scenarios.feature:2</span>
+              <h3 onclick="Collapsible_toggle('scenario_0')">
+                <span class="val">Scenario: Passing</span>
+              </h3>
+              <ol class="scenario_steps" id="scenario_0">
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Given </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Then </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+              </ol>
+            </div>
+            <div class="scenario">
+              <span class="scenario_file">features/feature_three_scenarios.feature:6</span>
+              <h3 class="failed" onclick="Collapsible_toggle('scenario_1')">
+                <span class="val">Scenario: Failing</span>
+              </h3>
+              <ol class="scenario_steps" id="scenario_1">
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Given </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step failed">
+                  <div class="step_name">
+                    <span class="keyword">Then </span>
+                    <span class="step val">a step fails</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:7</span>
+                  </div>
+                  <span class="embed"/>
+                  <a class="message" onclick="Collapsible_toggle('embed_1')">Error message</a>
+                  <pre id="embed_1" style="display: none">Assertion Failed: XFAIL-STEP</pre>
+
+                </li>
+              </ol>
+            </div>
+            <div class="scenario">
+              <span class="scenario_file">features/feature_three_scenarios.feature:10</span>
+              <h3 class="failed" onclick="Collapsible_toggle('scenario_2')">
+                <span class="val">Scenario: Failing with skipped steps</span>
+              </h3>
+              <ol class="scenario_steps" id="scenario_2">
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Given </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step failed">
+                  <div class="step_name">
+                    <span class="keyword">When </span>
+                    <span class="step val">a step fails</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:7</span>
+                  </div>
+                  <span class="embed"/>
+                  <a class="message" onclick="Collapsible_toggle('embed_2')">Error message</a>
+                  <pre id="embed_2" style="display: none">Assertion Failed: XFAIL-STEP</pre>
+
+                </li>
+              </ol>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on step with one parameter
+        Given a file named "features/feature_step_with_one_parameter.feature" with:
+            """
+            Feature:
+              Scenario: Simple scenario with one parameter in step
+                  Given a step passes
+                  When a step with parameter "foo" passes
+                  Then a step passes
+            """
+        When I run "behave -f html features/feature_step_with_one_parameter.feature"
+        Then it should pass with:
+            """
+            1 feature passed, 0 failed, 0 skipped
+            1 scenario passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="scenario">
+              <span class="scenario_file">features/feature_step_with_one_parameter.feature:2</span>
+                <h3 onclick="Collapsible_toggle('scenario_0')">
+                  <span class="val">Scenario: Simple scenario with one parameter in step</span>
+                </h3>
+              <ol class="scenario_steps" id="scenario_0">
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Given </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">When </span>
+                    <span class="step val">
+                      <span>a step with parameter &quot;</span>
+                      <b>foo</b>
+                      <span>&quot; passes</span>
+                    </span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:11</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Then </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                    <span class="embed"/>
+                </li>
+              </ol>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on step with several parameters
+        Given a file named "features/feature_step_with_parameters.feature" with:
+            """
+            Feature:
+              Scenario: Simple scenario with parameters in step
+                  Given a step passes
+                  When a step with parameter "foo" and parameter "bar" passes
+                  Then a step passes
+            """
+        When I run "behave -f html features/feature_step_with_parameters.feature"
+        Then it should pass with:
+            """
+            1 feature passed, 0 failed, 0 skipped
+            1 scenario passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <div class="scenario">
+              <span class="scenario_file">features/feature_step_with_parameters.feature:2</span>
+              <h3 onclick="Collapsible_toggle('scenario_0')">
+                <span class="val">Scenario: Simple scenario with parameters in step</span>
+              </h3>
+              <ol class="scenario_steps" id="scenario_0">
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Given </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">When </span>
+                    <span class="step val">
+                      <span>a step with parameter &quot;</span>
+                      <b>foo</b>
+                      <span>&quot; and parameter &quot;</span>
+                      <b>bar</b>
+                      <span>&quot; passes</span>
+                    </span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:15</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+                <li class="step passed">
+                  <div class="step_name">
+                    <span class="keyword">Then </span>
+                    <span class="step val">a step passes</span>
+                  </div>
+                  <div class="step_file">
+                    <span>features/steps/steps.py:3</span>
+                  </div>
+                  <span class="embed"/>
+                </li>
+              </ol>
+            </div>
+            """
+
+    Scenario: Use HTML formatter on step with multiline
+        Given a file named "features/feature_multiline_step.feature" with:
+            """
+            Feature:
+              Scenario: Simple scenario with multiline string in step
+                Given a step passes
+                When a step passes:
+                  '''
+                  Tiger, tiger, burning bright
+                  In the forests of the night,
+                  What immortal hand or eye
+                  Could frame thy fearful symmetry?
+                  '''
+                Then a step passes
+            """
+        When I run "behave -f html features/feature_multiline_step.feature"
+        Then it should pass with:
+            """
+            1 feature passed, 0 failed, 0 skipped
+            1 scenario passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <li class="step passed">
+              <div class="step_name">
+                <span class="keyword">When </span>
+                <span class="step val">a step passes</span>
+              </div>
+              <div class="step_file">
+                <span>features/steps/steps.py:3</span>
+              </div>
+              <span class="embed"/>
+              <div class="message">
+                <pre>Tiger, tiger, burning bright
+                In the forests of the night,
+                What immortal hand or eye
+                Could frame thy fearful symmetry?</pre>
+              </div>
+            </li>
+            """
+
+    Scenario: Use HTML formatter on step with table
+        Given a file named "features/feature_step_with_table.feature" with:
+            """
+            Feature: Step with table data
+              Scenario:
+                Given a step passes
+                When a step passes:
+                  | Name | Value |
+                  | Foo  | 42    |
+                  | Bar  | qux   |
+                Then a step passes
+            """
+        When I run "behave -f html features/feature_step_with_table.feature"
+        Then it should pass with:
+            """
+            1 feature passed, 0 failed, 0 skipped
+            1 scenario passed, 0 failed, 0 skipped
+            """
+        And the command output should contain:
+            """
+            <li class="step passed">
+              <div class="step_name">
+                <span class="keyword">When </span>
+                <span class="step val">a step passes</span>
+              </div>
+              <div class="step_file">
+                <span>features/steps/steps.py:3</span>
+              </div>
+              <span class="embed"/>
+              <table>
+                <tr>
+                  <th>Name</th>
+                  <th>Value</th>
+                </tr>
+                <tr>
+                  <td>Foo</td>
+                  <td>42</td>
+                </tr>
+                <tr>
+                  <td>Bar</td>
+                  <td>qux</td>
+                </tr>
+              </table>
+            </li>
+            """
diff --git a/issue.features/issue0031.feature b/issue.features/issue0031.feature
index 8f1b493..a0abb1c 100644
--- a/issue.features/issue0031.feature
+++ b/issue.features/issue0031.feature
@@ -8,9 +8,20 @@ Feature: Issue #31 "behave --format help" raises an error
     And the command output should contain:
       """
       Available formatters:
+        html           Very basic HTML formatter
         json           JSON dump of test run
         json.pretty    JSON dump of test run (human readable)
         null           Provides formatter that does not output anything.
         plain          Very basic formatter with maximum compatibility
         pretty         Standard colourised pretty formatter
+        progress       Shows dotted progress for each executed scenario.
+        progress2      Shows dotted progress for each executed step.
+        progress3      Shows detailed progress for each step of a scenario.
+        rerun          Emits scenario file locations of failing scenarios
+        sphinx.steps   Generate sphinx-based documentation for step definitions.
+        steps          Shows step definitions (step implementations).
+        steps.doc      Shows documentation for step definitions.
+        steps.usage    Shows how step definitions are used by steps.
+        tags           Shows tags (and how often they are used).
+        tags.location  Shows tags and the location where they are used.
       """
-- 
1.8.3.1