├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── README.rst ├── pyconcordion2 ├── __init__.py ├── base.py ├── commands.py ├── expression_parser.py ├── resources │ ├── embedded.css │ └── main.js └── tests │ ├── __init__.py │ ├── spec │ ├── Index.html │ ├── IndexTest.py │ ├── __init__.py │ ├── concordion │ │ ├── Concordion.html │ │ ├── ConcordionTest.py │ │ ├── Example.html │ │ ├── ExampleTest.py │ │ ├── __init__.py │ │ ├── command │ │ │ ├── Command.html │ │ │ ├── CommandTest.py │ │ │ ├── __init__.py │ │ │ ├── assertEquals │ │ │ │ ├── AssertEquals.html │ │ │ │ ├── AssertEqualsTest.py │ │ │ │ ├── Exceptions.html │ │ │ │ ├── ExceptionsTest.py │ │ │ │ ├── NestedActions.html │ │ │ │ ├── NestedActionsTest.py │ │ │ │ ├── NestedElements.html │ │ │ │ ├── NestedElementsTest.py │ │ │ │ ├── SupportedElements.html │ │ │ │ ├── SupportedElementsTest.py │ │ │ │ ├── __init__.py │ │ │ │ ├── nonString │ │ │ │ │ ├── NonString.html │ │ │ │ │ ├── NonStringTest.py │ │ │ │ │ ├── NullResult.html │ │ │ │ │ ├── NullResultTest.py │ │ │ │ │ ├── VoidResult.html │ │ │ │ │ ├── VoidResultTest.py │ │ │ │ │ └── __init__.py │ │ │ │ └── whitespace │ │ │ │ │ ├── LineContinuations.html │ │ │ │ │ ├── LineContinuationsTest.py │ │ │ │ │ ├── Normalization.html │ │ │ │ │ ├── NormalizationTest.py │ │ │ │ │ ├── Whitespace.html │ │ │ │ │ ├── WhitespaceTest.py │ │ │ │ │ └── __init__.py │ │ │ ├── assertFalse │ │ │ │ ├── AssertFalse.html │ │ │ │ ├── AssertFalseTest.py │ │ │ │ └── __init__.py │ │ │ ├── assertTrue │ │ │ │ ├── AssertTrue.html │ │ │ │ ├── AssertTrueTest.py │ │ │ │ └── __init__.py │ │ │ ├── echo │ │ │ │ ├── DisplayingNulls.html │ │ │ │ ├── DisplayingNullsTest.py │ │ │ │ ├── Echo.html │ │ │ │ ├── EchoTest.py │ │ │ │ ├── EscapingHtmlCharacters.html │ │ │ │ ├── EscapingHtmlCharactersTest.py │ │ │ │ └── __init__.py │ │ │ ├── execute │ │ │ │ ├── Execute.html │ │ │ │ ├── ExecuteTest.py │ │ │ │ ├── ExecutingTables.html │ │ │ │ ├── ExecutingTablesTest.py │ │ │ │ └── __init__.py │ │ │ ├── results │ │ │ │ ├── __init__.py │ │ │ │ ├── contentType │ │ │ │ │ ├── ContentType.html │ │ │ │ │ ├── ContentTypeTest.py │ │ │ │ │ └── __init__.py │ │ │ │ └── stylesheet │ │ │ │ │ ├── MissingHeadElement.html │ │ │ │ │ ├── MissingHeadElementTest.py │ │ │ │ │ ├── Stylesheet.html │ │ │ │ │ ├── StylesheetTest.py │ │ │ │ │ └── __init__.py │ │ │ ├── run │ │ │ │ ├── Run.html │ │ │ │ ├── RunTest.py │ │ │ │ └── __init__.py │ │ │ ├── set │ │ │ │ ├── Set.html │ │ │ │ ├── SetTest.py │ │ │ │ └── __init__.py │ │ │ └── verifyRows │ │ │ │ ├── TableBodySupport.html │ │ │ │ ├── TableBodySupportTest.py │ │ │ │ ├── VerifyRows.html │ │ │ │ ├── VerifyRowsTest.py │ │ │ │ ├── __init__.py │ │ │ │ └── results │ │ │ │ ├── MissingRows.html │ │ │ │ ├── MissingRowsTest.py │ │ │ │ ├── SurplusRows.html │ │ │ │ ├── SurplusRowsTest.py │ │ │ │ └── __init__.py │ │ ├── extension │ │ │ ├── CSSExtension.html │ │ │ ├── CSSExtensionTest.py │ │ │ ├── CustomCommand.html │ │ │ ├── CustomCommandTest.py │ │ │ ├── Extension.html │ │ │ ├── ExtensionConfiguration.html │ │ │ ├── ExtensionConfigurationTest.py │ │ │ ├── ExtensionTest.py │ │ │ ├── JavaScriptExtension.html │ │ │ ├── JavaScriptExtensionTest.py │ │ │ ├── ResourceExtension.html │ │ │ ├── ResourceExtensionTest.py │ │ │ ├── __init__.py │ │ │ └── listener │ │ │ │ ├── ExecuteTableListener.html │ │ │ │ ├── Listener.html │ │ │ │ ├── VerifyRowsListener.html │ │ │ │ └── __init__.py │ │ └── results │ │ │ ├── Results.html │ │ │ ├── ResultsTest.py │ │ │ ├── __init__.py │ │ │ ├── assertEquals │ │ │ ├── __init__.py │ │ │ ├── failure │ │ │ │ ├── Anchors.html │ │ │ │ ├── AnchorsTest.py │ │ │ │ ├── Empty.html │ │ │ │ ├── EmptyTest.py │ │ │ │ ├── Failure.html │ │ │ │ ├── FailureTest.py │ │ │ │ ├── NestedElements.html │ │ │ │ ├── NestedElementsTest.py │ │ │ │ └── __init__.py │ │ │ └── success │ │ │ │ ├── Empty.html │ │ │ │ ├── EmptyTest.py │ │ │ │ ├── HasAttributes.html │ │ │ │ ├── HasAttributesTest.py │ │ │ │ ├── HasClassAttribute.html │ │ │ │ ├── HasClassAttributeTest.py │ │ │ │ ├── Success.html │ │ │ │ ├── SuccessTest.py │ │ │ │ └── __init__.py │ │ │ └── assertTrue │ │ │ ├── __init__.py │ │ │ ├── failure │ │ │ ├── Failure.html │ │ │ ├── FailureTest.py │ │ │ └── __init__.py │ │ │ └── success │ │ │ ├── Success.html │ │ │ ├── SuccessTest.py │ │ │ └── __init__.py │ ├── examples │ │ ├── Demo.html │ │ ├── DemoTest.py │ │ ├── PartialMatches.html │ │ ├── PartialMatchesTest.py │ │ ├── Spike.html │ │ ├── SpikeTest.py │ │ └── __init__.py │ └── run_test.py │ ├── test_rig.py │ └── unit │ ├── __init__.py │ └── expression_parser_test.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | .idea/* 37 | *.iml 38 | 39 | # OS X crap 40 | .DS_Store 41 | 42 | # virtualenv 43 | env_concordion -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | env: 4 | global: 5 | - REPO="concordion/pyconcordion2" 6 | - CI_HOME=`pwd`/$REPO 7 | - PYTHONPATH="$CI_HOME/pyconcordion2:$PYTHONPATH" 8 | 9 | python: 10 | - "2.7" 11 | 12 | # command to install dependencies 13 | install: 14 | - pip install -r requirements.txt 15 | - pip install coveralls 16 | 17 | # command to run tests 18 | script: nosetests --with-coverage --cover-tests --cover-package=pyconcordion2 19 | 20 | after_success: 21 | coveralls 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | recursive-include pyconcordion2/resources * 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |Build Status| |Coverage Status| 2 | 3 | pyconcordion2 4 | ============= 5 | 6 | A python implementation of the Concordion Acceptance Testing framework. 7 | 8 | Installation 9 | ------------ 10 | 11 | ``$ pip install pyconcordion2`` 12 | 13 | Usage 14 | ----- 15 | 16 | Simply extend your python test cases from ConcordionTestCase 17 | 18 | ``from pyconcordion2 import ConcordionTestCase`` 19 | 20 | Execute as you would normal unittests. 21 | 22 | Key Differences 23 | --------------- 24 | 25 | This is not a 100% port of the original Concordion framework. If you 26 | found a differing behaviour please let me know. 27 | 28 | Differences: 29 | 30 | - Not possible to link to test data via CSV 31 | - Extensions are currently not supported. 32 | 33 | Development Setup 34 | ----------------- 35 | 36 | :: 37 | 38 | $ git clone git@github.com:concordion/pyconcordion2.git 39 | $ cd pyconcordion2 40 | $ virtualenv env_concordion 41 | $ source ./env_concordion/bin/activate 42 | $ pip install -r requirements.txt 43 | $ nosetests # to run tests 44 | 45 | Deploying on Pypi 46 | ----------------- 47 | 48 | :: 49 | $ python2 setup.py sdist bdist_wheel 50 | $ twine upload dist/* 51 | 52 | License 53 | ------- 54 | 55 | Copyright 2013 John Jiang 56 | 57 | Licensed under the Apache License, Version 2.0 (the "License"); you may 58 | not use this file except in compliance with the License. You may obtain 59 | a copy of the License at 60 | 61 | :: 62 | 63 | http://www.apache.org/licenses/LICENSE-2.0 64 | 65 | Unless required by applicable law or agreed to in writing, software 66 | distributed under the License is distributed on an "AS IS" BASIS, 67 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 68 | See the License for the specific language governing permissions and 69 | limitations under the License. 70 | 71 | Attribution 72 | ----------- 73 | 74 | Thanks to the `Concordion team`_ for making the original framework. 75 | 76 | Thanks to JC Plessis for making `pyconcordion python port`_ 77 | 78 | .. _Concordion team: http://www.concordion.org/ 79 | .. _pyconcordion python port: https://code.google.com/p/pyconcordion/ 80 | 81 | .. |Build Status| image:: https://travis-ci.org/concordion/pyconcordion2.png 82 | :target: https://travis-ci.org/concordion/pyconcordion2 83 | .. |Coverage Status| image:: https://coveralls.io/repos/concordion/pyconcordion2/badge.png 84 | :target: https://coveralls.io/r/concordion/pyconcordion2 85 | -------------------------------------------------------------------------------- /pyconcordion2/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from base import ConcordionTestCase 3 | 4 | __version__ = '0.15.1' 5 | -------------------------------------------------------------------------------- /pyconcordion2/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import inspect 3 | import logging 4 | import os 5 | import sys 6 | import tempfile 7 | import unittest 8 | 9 | from lxml import etree 10 | 11 | from commands import Commander 12 | 13 | logger = logging.getLogger(__file__) 14 | logger.level = logging.INFO 15 | 16 | TEMP_DIR = tempfile.gettempdir() 17 | 18 | 19 | class ConcordionTestCase(unittest.TestCase): 20 | extra_folder = "." 21 | 22 | stream_handler = logging.StreamHandler(sys.stdout) 23 | 24 | @classmethod 25 | def setUpClass(cls): 26 | logger.addHandler(cls.stream_handler) 27 | 28 | @classmethod 29 | def tearDownClass(cls): 30 | logger.removeHandler(cls.stream_handler) 31 | 32 | def runTest(self): 33 | # hack to prevent the base class to be run 34 | if self.__class__.__name__ == ConcordionTestCase.__name__: 35 | return True 36 | 37 | filename = self._find_spec() 38 | 39 | runner = Commander(self, filename) 40 | runner.process() 41 | 42 | runner.tree.xpath("//body")[0].insert(0, self.bread_crumb_tag()) 43 | self.__write(filename, runner.tree) 44 | self.assertTrue(runner.result.has_succeeded()) 45 | 46 | def _find_spec(self): 47 | """ 48 | We find the filename of the spec based on the name of the test. If the class ends in "test" we remove it. 49 | """ 50 | filename, ext = os.path.splitext(inspect.getfile(self.__class__)) 51 | if filename[-4:].lower() == "test": 52 | filename = filename[:-4] 53 | filename += ".html" 54 | with open(filename): # will raise exception if it doesn't exist 55 | return filename 56 | 57 | def bread_crumbs(self): 58 | head, tail = os.path.split(self.file_path()) 59 | crumbs = [] 60 | while True: 61 | head, tail = os.path.split(head) 62 | 63 | # we do this because capitalize() makes the first character uppercase and everything else lowercase 64 | base = os.path.join(head, tail, tail[0].upper() + tail[1:]) 65 | 66 | crumb = base + ".html" 67 | # we skip if we're looking at the current spec 68 | if crumb == self._find_spec(): 69 | continue 70 | if os.path.exists(crumb): 71 | crumbs.append(crumb) 72 | else: 73 | break 74 | return reversed(crumbs) 75 | 76 | def bread_crumb_tag(self): 77 | span_tag = etree.Element("span", {"class": "breadcrumbs"}) 78 | for crumb in self.bread_crumbs(): 79 | crumb_relpath = os.path.relpath(crumb, os.path.dirname(self.file_path())) 80 | a_tag = etree.Element("a", href=crumb_relpath) 81 | a_tag.text = os.path.splitext(os.path.basename(crumb_relpath))[0] 82 | a_tag.tail = " > " 83 | span_tag.append(a_tag) 84 | return span_tag 85 | 86 | def __write(self, filename, tree): 87 | output_dir = os.getenv('PYCONCORDION_OUTPUT', TEMP_DIR) 88 | output_dir = os.path.join(output_dir, self.extra_folder) 89 | if not os.path.isdir(output_dir): 90 | os.makedirs(output_dir) 91 | 92 | with open(os.path.join(output_dir, os.path.basename(filename)), "w") as f: 93 | logger.info("Saving to:\n%s" % f.name) 94 | f.write(etree.tostring(tree, pretty_print=True)) 95 | 96 | def file_path(self): 97 | return os.path.abspath(inspect.getfile(self.__class__)) 98 | -------------------------------------------------------------------------------- /pyconcordion2/commands.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from collections import OrderedDict 3 | from io import BytesIO 4 | from operator import attrgetter 5 | import imp 6 | import inspect 7 | import itertools 8 | import os 9 | import re 10 | import traceback 11 | import unittest 12 | 13 | from enum import Enum 14 | from lxml import etree 15 | from lxml import html 16 | 17 | import expression_parser 18 | 19 | truth_values = ['true', '1', 't', 'y', 'yes'] 20 | 21 | CHAR_SPACE = '\u00A0' 22 | 23 | CONCORDION_NAMESPACE = "http://www.concordion.org/2007/concordion" 24 | 25 | 26 | class Status(Enum): 27 | success = 1 28 | failure = 2 29 | ignored = 3 30 | 31 | 32 | class ResultEvent(object): 33 | def __init__(self, actual, expected): 34 | self.actual = actual 35 | self.expected = expected 36 | 37 | 38 | class Result(object): 39 | def __init__(self, tree): 40 | self.root_element = tree 41 | self.successes = tree.xpath("//*[contains(concat(' ', @class, ' '), ' success ')]") 42 | self.failures = tree.xpath("//*[contains(concat(' ', @class, ' '), ' failure ')]") 43 | self.missing = tree.xpath("//*[contains(concat(' ', @class, ' '), ' missing ')]") 44 | self.exceptions = tree.xpath("//*[contains(concat(' ', @class, ' '), ' exceptionMessage ')]") 45 | 46 | def last_failed_event(self): 47 | last_failed = self.failures[-1] 48 | actual = last_failed.xpath("//*[@class='actual']")[0].text 49 | expected = last_failed.xpath("//*[@class='expected']")[0].text 50 | return ResultEvent(actual, expected) 51 | 52 | @property 53 | def num_failure(self): 54 | return len(self.failures) 55 | 56 | @property 57 | def num_exception(self): 58 | return len(self.exceptions) 59 | 60 | @property 61 | def num_missing(self): 62 | return len(self.missing) 63 | 64 | @property 65 | def num_success(self): 66 | return len(self.successes) 67 | 68 | def has_failed(self): 69 | return bool(self.num_failure or self.num_exception or self.num_missing) 70 | 71 | def has_succeeded(self): 72 | return not self.has_failed() 73 | 74 | 75 | class Commander(object): 76 | def __init__(self, test, filename): 77 | self.test = test 78 | self.filename = filename 79 | self.tree = etree.parse(self.filename) 80 | self.args = {} 81 | self.commands = OrderedDict() 82 | 83 | def process(self): 84 | """ 85 | 1. Finds all concordion elements 86 | 2. Iterates over concordion attributes 87 | 3. Generates ordered dictionary of commands 88 | 4. Executes commands in order 89 | """ 90 | elements = self.__find_concordion_elements() 91 | 92 | for element in elements: 93 | for key, expression_str in element.attrib.items(): 94 | if CONCORDION_NAMESPACE not in key: # we ignore any attributes that are not concordion 95 | continue 96 | 97 | key = key.replace("{%s}" % CONCORDION_NAMESPACE, "") # we remove the namespace 98 | 99 | command_cls = command_mapper.get(key) 100 | command = command_cls(element, expression_str, self.test) 101 | 102 | if element.tag.lower() == "th": 103 | index = self.__find_th_index(element) 104 | command.index = index 105 | 106 | self.__add_to_commands_dict(command) 107 | 108 | self.__run_commands() 109 | self.__postprocess_tree() 110 | self.result = Result(self.tree) 111 | 112 | def __find_concordion_elements(self): 113 | """ 114 | Retrieves all etree elements with the concordion namespace 115 | """ 116 | return self.tree.xpath("""//*[namespace-uri()='{namespace}' or @*[namespace-uri()='{namespace}']]""".format( 117 | namespace=CONCORDION_NAMESPACE)) 118 | 119 | def __find_th_index(self, element): 120 | """ 121 | Returns the index of the given table header cell 122 | """ 123 | parent = element.getparent() 124 | for index, th_element in enumerate(parent.xpath("th")): 125 | if th_element == element: 126 | return index 127 | raise RuntimeError("Could not match command with table header") # should NEVER happen 128 | 129 | def __add_to_commands_dict(self, command): 130 | """ 131 | Given a command, we check to see if it's a child of another command. If it is we add it to the list of child 132 | commands. Otherwise we set it as a brand new command 133 | """ 134 | element = command.element 135 | while element.getparent() is not None: 136 | if element.getparent() in self.commands: 137 | self.commands[element.getparent()].children.append(command) 138 | return 139 | else: 140 | element = element.getparent() 141 | self.commands[command.element] = command 142 | 143 | def __run_commands(self): 144 | """ 145 | Runs each command in order 146 | """ 147 | for element, command in self.commands.items(): 148 | command.run() 149 | 150 | def __postprocess_tree(self): 151 | css_path = os.path.join(os.path.dirname(__file__), "resources", "embedded.css") 152 | css_contents = open(css_path, "rU").read() 153 | 154 | js_path = os.path.join(os.path.dirname(__file__), "resources", "main.js") 155 | js_contents = open(js_path, "rU").read() 156 | 157 | meta = etree.Element("meta") 158 | meta.attrib["http-equiv"] = "content-type" 159 | meta.attrib["content"] = "text/html; charset=UTF-8" 160 | meta.tail = "\n" 161 | 162 | head = self.tree.xpath("//head") 163 | if head: 164 | head[0].insert(0, meta) 165 | else: 166 | head = etree.Element("head") 167 | head.text = "\n" 168 | head.append(meta) 169 | 170 | for child in self.tree.getroot().getchildren(): 171 | if child.tag == "body": 172 | break 173 | head.append(child) 174 | head.tail = "\n" 175 | 176 | self.tree.getroot().insert(0, head) 177 | 178 | head = self.tree.xpath("//head")[0] 179 | style_tag = etree.Element("style", type="text/css") 180 | style_tag.text = css_contents 181 | head.insert(0, style_tag) 182 | 183 | js_tag = etree.Element("script") 184 | js_tag.text = js_contents 185 | jquery_tag = etree.Element("script", src="http://code.jquery.com/jquery-1.10.2.min.js") 186 | jquery_tag.text = " " 187 | 188 | self.tree.getroot().append(jquery_tag) 189 | self.tree.getroot().append(js_tag) 190 | 191 | 192 | class Command(object): 193 | def __init__(self, element, expression_str, context): 194 | self.element = element 195 | self.expression_str = expression_str.replace("#", "") 196 | self.context = context 197 | self.children = [] 198 | 199 | def _run(self): 200 | raise NotImplementedError 201 | 202 | def run(self): 203 | try: 204 | self.context.TEXT = get_element_content(self.element) 205 | self._run() 206 | return True 207 | except Exception as e: 208 | mark_exception(self.element, e) 209 | 210 | 211 | class RunCommand(Command): 212 | def _run(self): 213 | href = self.element.attrib["href"].replace(".html", "") 214 | f = inspect.getfile(self.context.__class__) 215 | file_path = os.path.join(os.path.dirname(os.path.abspath(f)), href) 216 | 217 | if os.path.exists(file_path + ".py"): 218 | src_file_path = file_path + ".py" 219 | elif os.path.exists(file_path + "Test.py"): 220 | src_file_path = file_path + "Test.py" 221 | else: 222 | raise RuntimeError("Cannot find Python Test file") 223 | 224 | modname, ext = os.path.splitext(os.path.basename(src_file_path)) 225 | test_class = imp.load_source(modname, src_file_path) 226 | 227 | test_class = getattr(test_class, modname)() 228 | test_class.extra_folder = os.path.dirname(os.path.join(self.context.extra_folder, href)) 229 | result = unittest.TextTestRunner().run(test_class) 230 | if result.failures or result.errors: 231 | mark_status(Status.failure, self.element) 232 | elif result.expectedFailures: 233 | mark_status(Status.ignored, self.element) 234 | else: 235 | mark_status(Status.success, self.element) 236 | 237 | 238 | class ExecuteCommand(Command): 239 | def _run(self): 240 | if self.element.tag.lower() == "table": 241 | for row in get_table_body_rows(self.element): 242 | for command in self.children: 243 | td_element = row.xpath("td")[command.index] 244 | command.element = td_element 245 | self._run_children() 246 | else: 247 | self._run_children() 248 | 249 | def _run_children(self): 250 | for command in self.children: 251 | if isinstance(command, SetCommand): 252 | command.run() 253 | expression_parser.execute_within_context(self.context, self.expression_str) 254 | for command in self.children: 255 | if not isinstance(command, SetCommand): 256 | command.run() 257 | 258 | 259 | class VerifyRowsCommand(Command): 260 | def _run(self): 261 | variable_name = expression_parser.parse(self.expression_str).variable_name 262 | results = expression_parser.execute_within_context(self.context, self.expression_str) 263 | for result, row in itertools.izip_longest(results, get_table_body_rows(self.element)): 264 | setattr(self.context, variable_name, result) 265 | 266 | if result is None: 267 | row.attrib["class"] = (row.attrib.get("class", "") + " missing").strip() 268 | continue 269 | 270 | if row is None: 271 | total_columns = max(self.children, key=attrgetter("index")).index + 1 # good enough but not perfect 272 | row = etree.Element("tr", **{"class": "surplus"}) 273 | for _ in xrange(total_columns): 274 | etree.SubElement(row, "td") 275 | if self.element.xpath("//tbody"): 276 | self.element.xpath("//tbody")[0].append(row) 277 | else: 278 | self.element.append(row) 279 | 280 | for command in self.children: 281 | element = row.xpath("td")[command.index] 282 | command.element = element 283 | command.run() 284 | 285 | 286 | def get_table_body_rows(table): 287 | if table.xpath("//tbody"): 288 | tr_s = table.xpath("//tbody/tr") 289 | else: 290 | tr_s = table.xpath("tr") 291 | return [tr for tr in tr_s if tr.xpath("td")] 292 | 293 | 294 | def normalize(text): 295 | text = unicode(text) 296 | text = text.replace(" _\n", "") # support for python style line breaks 297 | pattern = re.compile(r'\s+') # treat all whitespace as spaces 298 | return re.sub(pattern, ' ', text).strip() 299 | 300 | 301 | def get_element_content(element): 302 | tag_html = html.parse(BytesIO(etree.tostring(element))).getroot().getchildren()[0].getchildren()[0] 303 | return normalize(tag_html.text_content()) 304 | 305 | 306 | class SetCommand(Command): 307 | def _run(self): 308 | expression = expression_parser.parse(self.expression_str) 309 | if expression.function_name: # concordion:set="blah = function(#TEXT)" 310 | expression_parser.execute_within_context(self.context, self.expression_str) 311 | else: 312 | setattr(self.context, expression.variable_name, get_element_content(self.element)) 313 | 314 | 315 | class AssertEqualsCommand(Command): 316 | def _run(self): 317 | expression_return = expression_parser.execute_within_context(self.context, self.expression_str) 318 | if expression_return is None: 319 | expression_return = "(None)" 320 | 321 | result = normalize(expression_return) == get_element_content(self.element) 322 | if result: 323 | mark_status(Status.success, self.element) 324 | else: 325 | mark_status(Status.failure, self.element, expression_return) 326 | 327 | 328 | class AssertTrueCommand(Command): 329 | def _run(self): 330 | result = expression_parser.execute_within_context(self.context, self.expression_str) 331 | if result: 332 | mark_status(Status.success, self.element) 333 | else: 334 | mark_status(Status.failure, self.element, "== false") 335 | 336 | 337 | class AssertFalseCommand(Command): 338 | def _run(self): 339 | result = expression_parser.execute_within_context(self.context, self.expression_str) 340 | if not result: 341 | mark_status(Status.success, self.element) 342 | else: 343 | mark_status(Status.failure, self.element, "== true") 344 | 345 | 346 | class EchoCommand(Command): 347 | def _run(self): 348 | result = expression_parser.execute_within_context(self.context, self.expression_str) 349 | if result is not None: 350 | self.element.text = result 351 | else: 352 | em = etree.Element("em") 353 | em.text = "None" 354 | self.element.append(em) 355 | 356 | 357 | def mark_status(status, element, actual_value=None): 358 | if not get_element_content(element): # set non-breaking space if element is empty 359 | element.text = CHAR_SPACE 360 | 361 | element.attrib["class"] = (element.attrib.get("class", "") + " {}".format(status.name)).strip() 362 | if actual_value is not None: 363 | actual = etree.Element("ins", **{"class": "actual"}) 364 | actual.text = unicode(actual_value) or CHAR_SPACE # blank space if no value 365 | 366 | # we move child elements from element into our new del container 367 | expected = etree.Element("del", **{"class": "expected"}) 368 | for child in element.getchildren(): 369 | expected.append(child) 370 | expected.text = element.text 371 | element.text = None 372 | expected.tail = "\n" 373 | 374 | element.insert(0, expected) 375 | element.insert(1, actual) 376 | 377 | 378 | __exception_index = 1 379 | 380 | 381 | def mark_exception(target_element, e): 382 | global __exception_index 383 | exception_element = etree.Element("span", **{"class": "exceptionMessage"}) 384 | exception_element.text = unicode(e) 385 | 386 | input_element = etree.Element("input", 387 | **{"class": "stackTraceButton", "data-exception-index": unicode(__exception_index), 388 | "type": "button", "value": "Toggle Stack"}) 389 | 390 | stacktrace_div_element = etree.Element("div", **{"class": "stackTrace {}".format(__exception_index)}) 391 | p_tag = etree.Element("p") 392 | p_tag.text = "Traceback:" 393 | stacktrace_div_element.append(p_tag) 394 | tb = traceback.format_exc() 395 | for line in tb.splitlines(): 396 | trace_element = etree.Element("div", **{"class": "stackTraceEntry"}) 397 | trace_element.text = line 398 | stacktrace_div_element.append(trace_element) 399 | 400 | parent = target_element.getparent() 401 | # we insert the exception after the element in question 402 | for i, element in enumerate((exception_element, input_element, stacktrace_div_element)): 403 | parent.insert(parent.index(target_element) + 1 + i, element) 404 | 405 | __exception_index += 1 406 | 407 | 408 | command_mapper = { 409 | "run": RunCommand, 410 | "execute": ExecuteCommand, 411 | "set": SetCommand, 412 | "assertEquals": AssertEqualsCommand, 413 | "assertTrue": AssertTrueCommand, 414 | "assertFalse": AssertFalseCommand, 415 | "verifyRows": VerifyRowsCommand, 416 | "echo": EchoCommand 417 | } 418 | -------------------------------------------------------------------------------- /pyconcordion2/expression_parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from pyparsing import Word, alphanums, ZeroOrMore, Suppress, Optional, Group 4 | 5 | # define grammar 6 | variable_name = Word(alphanums + "_").setResultsName("variable_name") 7 | property_name = Word(alphanums + "_").setResultsName("property_name") 8 | function_name = Word(alphanums + "_").setResultsName("function_name") 9 | parameter_name = Word(alphanums + "_") 10 | equals = Suppress("=") 11 | colon = Suppress(":") 12 | open_parenthesis = Suppress("(") 13 | closed_parenthesis = Suppress(")") 14 | dot = Suppress(".") 15 | comma = Suppress(",") 16 | function_definition = function_name + open_parenthesis + Group( 17 | Optional(parameter_name + ZeroOrMore(comma + parameter_name))).setResultsName("parameters") + closed_parenthesis 18 | 19 | expression = function_definition | variable_name + dot + property_name | ( 20 | variable_name + Optional((equals | colon) + function_definition)) 21 | 22 | 23 | def parse(expression_str): 24 | return expression.parseString(expression_str) 25 | 26 | 27 | def execute_within_context(context, expression_str): 28 | expression_tree = parse(expression_str) 29 | if expression_tree.function_name: 30 | fn_name = getattr(context, expression_tree.function_name) 31 | parameters = [getattr(context, parameter) for parameter in expression_tree.parameters] 32 | result = fn_name(*parameters) 33 | if expression_tree.variable_name: 34 | setattr(context, expression_tree.variable_name, result) 35 | return result 36 | elif expression_tree.variable_name: 37 | variable = getattr(context, expression_tree.variable_name) 38 | if expression_tree.property_name: 39 | _property = getattr(variable, expression_tree.property_name) 40 | return _property 41 | return variable 42 | -------------------------------------------------------------------------------- /pyconcordion2/resources/embedded.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Arial; 3 | } 4 | body { 5 | padding: 32px; 6 | } 7 | pre { 8 | padding: 6px 28px 6px 28px; 9 | background-color: #E8EEF7; 10 | } 11 | pre, pre *, code, code *, kbd { 12 | font-family: Courier New, Courier; 13 | font-size: 10pt; 14 | } 15 | h1, h1 * { 16 | font-size: 24pt; 17 | } 18 | p, td, th, li, .breadcrumbs { 19 | font-size: 10pt; 20 | } 21 | p, li { 22 | line-height: 140%; 23 | max-width: 720px; 24 | } 25 | table { 26 | border-collapse: collapse; 27 | empty-cells: show; 28 | margin: 8px 0px 8px 0px; 29 | } 30 | th, td { 31 | border: 1px solid black; 32 | padding: 3px; 33 | } 34 | td { 35 | background-color: white; 36 | vertical-align: top; 37 | } 38 | th { 39 | background-color: #C3D9FF; 40 | } 41 | li { 42 | margin-top: 6px; 43 | margin-bottom: 6px; 44 | } 45 | 46 | .example { 47 | padding: 6px 16px 6px 16px; 48 | border: 1px solid #C3D9FF; 49 | margin: 6px 0px 28px 0px; 50 | background-color: #F5F9FD; 51 | } 52 | .example h3 { 53 | margin-top: 8px; 54 | margin-bottom: 8px; 55 | font-size: 12pt; 56 | } 57 | 58 | p.success { 59 | padding: 2px; 60 | } 61 | .success, .success * { 62 | background-color: #afa !important; 63 | } 64 | .success pre { 65 | background-color: #bbffbb; 66 | } 67 | .failure, .failure * { 68 | background-color: #ffb0b0; 69 | padding: 1px; 70 | } 71 | .failure .expected { 72 | text-decoration: line-through; 73 | color: #bb5050; 74 | } 75 | .ignored, .ignored * { 76 | background-color: #f0f0f0 !important; 77 | } 78 | 79 | ins { 80 | text-decoration: none; 81 | } 82 | 83 | .exceptionMessage { 84 | background-color: #fdd; 85 | font-family: Courier New, Courier, Monospace; 86 | font-size: 10pt; 87 | display: block; 88 | font-weight: normal; 89 | padding: 4px; 90 | text-decoration: none !important; 91 | } 92 | .stackTrace, .stackTrace * { 93 | font-weight: normal; 94 | } 95 | .stackTrace { 96 | display: none; 97 | padding: 1px 4px 4px 4px; 98 | background-color: #fdd; 99 | border-top: 1px dotted black; 100 | } 101 | .stackTraceExceptionMessage { 102 | display: block; 103 | font-family: Courier New, Courier, Monospace; 104 | font-size: 8pt; 105 | white-space: wrap; 106 | padding: 1px 0px 1px 0px; 107 | } 108 | .stackTraceEntry { 109 | white-space: nowrap; 110 | font-family: Courier New, Courier, Monospace; 111 | display: block; 112 | font-size: 8pt; 113 | padding: 1px 0px 1px 32px; 114 | } 115 | .stackTraceButton { 116 | font-size: 8pt; 117 | margin: 2px 8px 2px 0px; 118 | font-weight: normal; 119 | font-family: Arial; 120 | } 121 | 122 | .special { 123 | font-style: italic; 124 | } 125 | .missing, .missing * { 126 | background-color: #ff9999; 127 | color:#bb5050; 128 | text-decoration: line-through; 129 | } 130 | .surplus, .surplus * { 131 | background-color: #ff9999; 132 | } 133 | .footer { 134 | text-align: right; 135 | margin-top: 40px; 136 | font-size: 8pt; 137 | width: 100%; 138 | color: #999; 139 | } 140 | .footer .testTime { 141 | padding: 2px 10px 0px 0px; 142 | } 143 | 144 | .idea { 145 | font-size: 9pt; 146 | color: #888; 147 | font-style: italic; 148 | } 149 | .tight li { 150 | margin-top: 1px; 151 | margin-bottom: 1px; 152 | } 153 | .commentary { 154 | float: right; 155 | width: 200px; 156 | background-color: #ffffd0; 157 | padding:8px; 158 | border: 3px solid #eeeeb0; 159 | margin: 10px 0px 10px 10px; 160 | } 161 | .commentary, .commentary * { 162 | font-size: 8pt; 163 | } -------------------------------------------------------------------------------- /pyconcordion2/resources/main.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $(".stackTraceButton").on("click", function(event) { 3 | $(".stackTrace." + $(this).data("exception-index")).toggle(); 4 | }) 5 | }); 6 | -------------------------------------------------------------------------------- /pyconcordion2/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/concordion/pyconcordion2/49287e72687e0c14c459bdee56e0b62ba6b003c0/pyconcordion2/tests/__init__.py -------------------------------------------------------------------------------- /pyconcordion2/tests/spec/Index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |19 | The Concordion framework 20 | brings together testing and specification. 21 | Concrete examples of expected behaviour (written in HTML) can be 22 | instrumented with Concordion commands that run tests against the 23 | real system via some Java fixture code. 24 | The fixture code helps to decouple the specs from the system. 25 |
26 | 27 |28 | See here for a simple example. 29 |
30 | 31 |8 | Here's a very simple example that demonstrates the basic behaviour of Concordion. 9 | Note that the HTML specifications, below, have a Concordion namespace declaration at the top 10 | and we use the <span> tag around the variable that we're interested in checking. 11 |
12 | 13 |16 | Assuming we have Java fixture code containing a method: 17 |
18 | 19 |20 | public String getGreeting() { 21 | return "Hello World!"; 22 | } 23 |24 | 25 |
29 | When we run the following active specification it should report a
30 | success, since the
31 | expectation in the specification (Hello World!
)
32 | matches the actual result of the method.
33 |
36 | <html xmlns:concordion="http://www.concordion.org/2007/concordion"> 37 | <body> 38 | <p> 39 | The greeting should be: 40 | <span concordion:assertEquals="greeting()">Hello World!</span> 41 | </p> 42 | </body> 43 | </html> 44 |45 | 46 | 47 |
52 | On the other hand, this specfication should report a
53 | failure, since the
54 | expectation in the specification (Hello Bob!
) does not match
55 | the result of the method (Hello World!
).
56 |
59 | <html xmlns:concordion="http://www.concordion.org/2007/concordion"> 60 | <body> 61 | <p> 62 | The greeting should be: 63 | <span concordion:assertEquals="greeting()">Hello Bob!</span> 64 | </p> 65 | </body> 66 | </html> 67 |68 | 69 |
8 | The following commands are available: 9 |
10 | 11 |
8 | The assertEquals
command evaluates an expression and compares
9 | the result to the contents of an element in the document. The command reports
10 | a success if the evaluation result is equal to the text in the document,
11 | or a failure otherwise. The comparisons are case-sensitive.
12 |
Given this instrumentation:
19 | 20 |<span concordion:assertEquals="#user.firstName">Fred</span>21 | 22 |
We get the following outcomes depending on the evaluation result of the expression #user.firstName
:
Evaluation Result | 27 |Outcome | 28 |
---|---|
Fred | 31 |SUCCESS | 32 |
Wilma | 35 |FAILURE | 36 |
fred | 39 |FAILURE | 40 |
8 | If an exception is thrown during the evaluation of the expression, 9 | then the exception is reported. No "success" or "failure" events 10 | are reported since the exception will be treated as an implicit failure. 11 |
12 | 13 |Given the following instrumentation:
18 | 19 |20 | <span concordion:assertEquals="myMethod()">ABCD</span> 21 |22 | 23 |
And given that each row in this table is an independent test, we 24 | expect these event counts: 25 |
26 | 27 |Scenario | 30 |Event Counts | 31 |||
---|---|---|---|
myMethod() Returns | 34 |Successes | 35 |Failures | 36 |Exceptions | 37 |
ABCD | 40 |1 | 41 |0 | 42 |0 | 43 |
XYZ | 46 |0 | 47 |1 | 48 |0 | 49 |
(An exception) | 52 |0 | 53 |0 | 54 |1 | 55 |
8 | Though nested elements are supported, these 9 | elements may not contain commands. An "illegal markup" exception will be reported if 10 | any nested commands are detected. 11 |
12 | 13 |The following will result in an "illegal markup" exception being raised:
18 | 19 |20 | <span concordion:assertEquals="#fullName">Fred 21 | <span concordion:set="#surname">Bloggs</span> 22 | </span> 23 |24 | 25 |
8 | Nested HTML elements are supported. The content of those elements is included in 9 | the expected string, but the tags themselves are not. 10 |
11 | 12 |18 | <span concordion:assertEquals="#fullName">Fred <strong>Bloggs</strong></span> 19 |20 | 21 |
22 | Will match
23 | the evaluation result "Fred Bloggs
".
24 |
No extra whitespace is added around the nested elements.
28 | 29 |34 | <span concordion:assertEquals="#fullName">Fred<em>Bloggs</em></span> 35 |36 | 37 |
38 |
39 | Will match the string
40 | "FredBloggs
", but will
41 |
42 |
43 |
44 | not match
45 | "Fred Bloggs
".
46 |
47 |
8 | The assertEquals
command can currently be used on any HTML element.
9 | There are currently no checks.
10 |
11 | However, you should avoid using the command on elements that cannot legally
12 | have a <span>
child (e.g. <table>
,
13 | <tr>
, <ul>
, <ol>
).
14 |
All of these will work:
21 | 22 |23 | <span concordion:assertEquals="#name">Fred</span> 24 |25 | 26 |
27 | <strong concordion:assertEquals="#name">Fred</strong> 28 |29 | 30 |
31 | <div concordion:assertEquals="#name">Fred</div> 32 |33 | 34 |
35 | <table> 36 | <tr> 37 | <td concordion:assertEquals="#name">Fred</td> 38 | </tr> 39 | </table> 40 |41 | 42 |
8 | Before comparison, the evaluation result is turned into a string by
9 | calling the object's toString()
method.
10 |
17 | <span concordion:assertEquals="myMethod()">(some expectation)</span> 18 |19 | 20 |
myMethod() Returns |
23 | Type | 24 |The Expectation | 25 |Outcome | 26 |
---|---|---|---|
1234 | 29 |String | 30 |1234 | 31 |SUCCESS | 32 |
1234 | 35 |Integer | 36 |1234 | 37 |SUCCESS | 38 |
99 | 41 |Integer | 42 |1234 | 43 |FAILURE | 44 |
1234 | 47 |Double | 48 |1234 | 49 |FAILURE | 50 |
1234 | 53 |Double | 54 |1234.0 | 55 |SUCCESS | 56 |
8 | If the evaluation result is None
then the string
9 | "(None)" is used for performing the comparison.
10 |
17 | <span concordion:assertEquals="myMethod()">(some expectation)</span> 18 |19 | 20 |
myMethod() Returns |
23 | The Expectation | 24 |Outcome | 25 |
---|---|---|
None | 28 |(None) | 29 |SUCCESS | 30 |
None | 33 |xyz | 34 |FAILURE | 35 |
None | 38 |None | 39 |FAILURE | 40 |
8 | If an expression returns a method that returns void
then
9 | the result is treated as null
.
10 |
Given we have the following method in our fixture code:
17 | 18 |19 | public void myVoidMethod() { 20 | ... 21 | } 22 |23 | 24 |
26 | Then the following markup will cause a SUCCESS
27 | to be reported, since no return value can be obtained from myVoidMethod()
28 | the result is taken to be None
:
29 |
32 | <span concordion:assertEquals="myVoidMethod()">(None)</span> 33 |34 | 35 |
39 | And, to demonstrate a counter-example, the following markup will report 40 | a FAILURE: 41 |
42 | 43 |44 | <span concordion:assertEquals="myVoidMethod()">xyz</span> 45 |46 |
8 | It is sometimes useful to be able to break a long line of text 9 | over several lines without the linebreaks being treated as whitespace. 10 |
11 | 12 |13 | You can do this by putting an underscore at the end of a line, preceded 14 | by a space. 15 |
16 | 17 |These two statements are treated identically:
22 | 23 | (1) 24 |<pre concordion:assertEquals="#firstName">Fred Flintstone</pre>25 | 26 | (2) 27 |
<pre concordion:assertEquals="#firstName">Fred Flint _ 28 | stone</pre>29 | 30 | 31 |
#firstName | 34 |Success | 35 |Failure | 36 |
---|---|---|
Fred Flintstone | 39 |(1), (2) | 40 |41 | |
Fred Flint stone | 44 |45 | | (1), (2) | 46 |
Fred Flint _ stone | 49 |50 | | (1), (2) | 51 |
Whitespace characters are:
8 | 9 |Source String | 23 |Normalized | 24 |
---|---|
fred | 27 |fred | 28 |
[SPACE]fred | 31 |fred | 32 |
[SPACE]fred[SPACE] | 35 |fred | 36 |
[SPACE][SPACE]fred[LF][SPACE] | 39 |fred | 40 |
[SPACE][CR][LF][TAB][SPACE]fred[SPACE][SPACE]bloggs | 43 |fred[SPACE]bloggs | 44 |
fred[LF][SPACE][TAB][CR][LF]bloggs | 47 |fred[SPACE]bloggs | 48 |
fred[LF][SPACE]x[TAB][CR][LF]bloggs | 51 |fred[SPACE]x[SPACE]bloggs | 52 |
8 | Whitespace for an assertEquals
is treated in roughly the
9 | same way that web browsers treat it.
10 |
13 | Whitespace at the start and end of the string is removed before comparison 14 | and whitespace in the middle of a string is compressed so that multiple 15 | whitespace characters become a single space character. The same "normalization" 16 | is performed on both the text read from the document and the string result 17 | of the evaluation. 18 |
19 | 20 |<span concordion:assertEquals="#firstName">Fred Flintstone</span>25 | 26 |
<span concordion:assertEquals="#firstName"> Fred 27 | Flintstone 28 | </span>29 | 30 |
31 | If #firstName
returns "Fred Flintstone
",
32 | both
33 | the above statements will report a success.
34 |
37 | If #firstName
returns " Fred Flintstone
",
38 | both
39 | the above statements will report a success.
40 |
43 | If #firstName
returns "Wilma Flintstone
",
44 | both
45 | the above statements will report a failure.
46 |
8 | The concordion:assertFalse
tag evaluates an expression
9 | to obtain a boolean result. If the result is true, the tagged element
10 | is marked as a success else it is marked as a failure.
11 |
Given this instrumented snippet:
18 | 19 |20 | <p> 21 | Since the password was incorrect, the user should not be 22 | <span concordion:assertFalse="#user.isLoggedIn()">logged in</span>. 23 | </p> 24 |25 | 26 |
We get the following outcomes depending on the evaluation result of the expression #user.isLoggedIn()
:
Evaluation Result | 31 |Outcome | 32 |
---|---|
true | 35 |FAILURE | 36 |
false | 39 |SUCCESS | 40 |
8 | The concordion:assertTrue
tag evaluates an expression
9 | to obtain a boolean result. If the result is true, the tagged element
10 | is marked as a success else it is marked as a failure.
11 |
Given this instrumented snippet:
18 | 19 |20 | <p> 21 | Since the password was correct, the user should then be 22 | <span concordion:assertTrue="#user.isLoggedIn()">logged in</span>. 23 | </p> 24 |25 | 26 |
We get the following outcomes depending on the evaluation result of the expression #user.isLoggedIn()
:
Evaluation Result | 31 |Outcome | 32 |
---|---|
true | 35 |SUCCESS | 36 |
false | 39 |FAILURE | 40 |
8 | Nones are wrapped in <em>
tags (i.e. <em>None</em>
) to
9 | make a visual distinction between the string "None" and a None value.
10 |
13 | The value is appended to any child markup. 14 |
15 | 16 |21 | Given the expression "username" returns a None, we expect the following results: 22 |
23 | 24 |25 | We have stripped out the concordion:echo attributes from the 26 | "expected output" for legibility. They may be present. 27 |
28 | 29 |Instrumentation | 32 |Expected output | 33 |
---|---|
<span concordion:echo="username" /> |
36 | <span><em>None</em></span> |
37 |
<span concordion:echo="username"></span> |
40 | <span><em>None</em></span> |
41 |
<span concordion:echo="username">abc</span> |
44 | <span>abc<em>None</em></span> |
45 |
<span concordion:echo="username"><b>abc</b></span> |
48 | <span><b>abc</b><em>None</em></span> |
49 |
8 | The concordion:echo
tag evaluates an expression and inserts
9 | the result into the output HTML.
10 |
17 | If the expression "username" 18 | evaluates to "jbloggs" and we 19 | have the following instrumentation in our specification: 20 |
21 | 22 |23 | <p> 24 | Username: <span concordion:echo="username" /> 25 | </p> 26 |27 | 28 |
Then we expect the following output:
29 | 30 |31 | <p> 32 | Username: <span concordion:echo="username">jbloggs</span> 33 | </p> 34 |35 | 36 |
8 | Special characters <, > and & are escaped in the output. 9 |
10 | 11 |16 | Given this instrumentation: 17 |
18 |<span concordion:echo="username" />19 | 20 |
21 | We expect the following outputs, depending on the evaluation result of username: 22 |
23 | 24 |25 | We have stripped out the concordion:echo attributes from the 26 | "expected output" for legibility. They may be present. 27 |
28 | 29 |username | 32 |Expected output | 33 |
---|---|
abc | 36 |<span>abc</span> |
37 |
a&b | 40 |<span>a&b</span> |
41 |
a<bc | 44 |<span>a<bc</span> |
45 |
<&>abc | 48 |<span><&>abc</span> |
49 |
11 | An execute
command lets you run a method in the
12 | Java fixture code.
13 |
The following instrumentation:
20 |21 | <p concordion:execute="myMethod()">Some text goes here.</p> 22 |23 |
24 | Will
25 | call myMethod()
in the Java fixture code.
26 |
8 | The execute
command has special behaviour when placed on
9 | a <table>
element. Instead of executing once, it
10 | executes every detail row in the table and transfers the commands
11 | from the header row to each detail row.
12 |
19 | <table concordion:execute="#username = generateUsername(#fullName)"> 20 | <tr> 21 | <th concordion:set="#fullName">Full Name</th> 22 | <th concordion:assertEquals="#username">Username</th> 23 | </tr> 24 | <tr> 25 | <td>Fred Bloggs</td> 26 | <td>fredbloggs</td> 27 | </tr> 28 | <tr> 29 | <td>John Doe</td> 30 | <td>johndoe</td> 31 | </tr> 32 | <tr> 33 | <td>Winston Churchill</td> 34 | <td>winston</td> 35 | </tr> 36 | </table> 37 |38 | 39 |
40 | If the method generateUsername()
returns the
41 | full name in lowercase with spaces removed, when we run
42 | the test we expect:
43 | 2 successes and
44 | 1 failure and
45 | 0 exceptions
46 | to be reported.
47 |
48 | The failure will have an expected value of
49 | "winston
"
50 | and an actual value of
51 | "winstonchurchill
".
52 |
<thead>
and <tbody>
supported?10 | If the document <head> section does not contain content-type metadata, then a <meta> element will be automatically inserted, 11 | specifying a content-type with charset set to UTF-8. 12 |
13 | 14 |When this document is processed:
19 |20 | <html xmlns:concordion="http://www.concordion.org/2007/concordion"> 21 | <head> 22 | <title>My Title</title> 23 | </head> 24 | <body/> 25 | </html> 26 |27 | 28 |
, the following output will be produced:
29 | 30 |31 | <html xmlns:concordion="http://www.concordion.org/2007/concordion"> 32 | <head> 33 | <meta http-equiv="content-type" content="text/html; charset=UTF-8"/> 34 | <title>My Title</title> 35 | </head> 36 | <body/> 37 | </html> 38 |39 |
10 | If the document does not contain a <head> section then one will 11 | be automatically inserted as the first child of the <html> element. 12 | Any elements before the <body> section will be moved into the 13 | <head>. 14 |
15 | 16 |When this document is processed:
21 |22 | <html xmlns:concordion="http://www.concordion.org/2007/concordion"> 23 | <link href="my.css" rel="stylesheet" type="text/css" /> 24 | <title>My Title</title> 25 | <body> 26 | <p>Body content goes here.</p> 27 | </body> 28 | </html> 29 |30 | 31 |
, a <head> element will be inserted:
32 | 33 |34 | <html xmlns:concordion="http://www.concordion.org/2007/concordion"> 35 | <head> 36 | <link href="my.css" rel="stylesheet" type="text/css"/> 37 | <title>My Title</title> 38 | </head> 39 | <body> 40 | <p>Body content goes here.</p> 41 | </body> 42 | </html> 43 |44 |
12 | The output documents need some CSS to render 13 | successes, failures, stack traces etc. correctly. 14 |
15 | 16 |17 | The essential styles will be embedded at the top of the 18 | <head> section of the document. This allows 19 | user-supplied stylesheets to override the settings, 20 | if desired, and allows the document to perform 21 | its function without any external dependencies 22 | (this makes it easier to e-mail the output documents). 23 |
24 | 25 |If we process this document:
30 |31 | <html xmlns:concordion="http://www.concordion.org/2007/concordion"> 32 | <head> 33 | <title>Example</title> 34 | </head> 35 | <body> 36 | <p>Body content goes here.</p> 37 | </body> 38 | </html> 39 |40 | 41 |
42 | We expect the output document to have a 43 | <style> 44 | element inserted into the 45 | <head> section 46 | before 47 | the <title> element. 48 |
49 | 50 |51 | The <style> element 52 | should 53 | contain styles for CSS classes like 54 | ".success" and 55 | ".failure". 56 |
57 |
11 | The run
command lets you run another test from this
12 | test, in a similar way to JUnit test-suites. This can be a useful
13 | way to view progress on a set of acceptance tests for a story.
14 |
17 | The format is: 18 |
19 |20 | <a concordion:run="runner-name" href="relative-link">some link text</a>21 | 22 |
23 | The runner-name should normally be "concordion"
. However, it
24 | is possible to implement your own runners to run tests implemented in another tool.
25 |
Here we run the test for the 32 | set command 33 | using this HTML:
34 |35 | <a concordion:run="concordion" href="../set/Set.html">set command</a>36 | 37 |
11 | A set
command sets a temporary variable to the
12 | text contents of the instrumented element, so that it can
13 | be referenced by another command.
14 |
The following instrumentation:
21 |22 | <p> 23 | My name is <b concordion:set="#fullName">David Peterson</b>. 24 | 25 | <span concordion:execute="setUpUser(#fullName)" /> 26 | </p> 27 |28 |
29 | Calls the method setUpUser()
with the
30 | string value
31 | David Peterson
.
32 |
10 | Although web browsers (and even the XHTML 1.0 Strict DTD) accept HTML tables without
11 | explicit <thead>
and<tbody>
sections,
12 | some people prefer to put them in. The verifyRows
13 | command supports tables containing these sections.
14 |
Given the method getNames()
returns a Collection containing the names:
21 | John, Paul
22 |
The following instrumentation:
25 | 26 |27 | <table concordion:verifyRows="#name : getNames()"> 28 | <thead> 29 | <tr> 30 | <th concordion:assertEquals="#name">Name</th> 31 | </tr> 32 | </thead> 33 | <tbody> 34 | <tr> 35 | <td>John</td> 36 | </tr> 37 | </tbody> 38 | </table> 39 |40 | 41 |
Results in this output:
42 | 43 |44 | <table concordion:verifyRows="#name : getNames()"> 45 | <thead> 46 | <tr> 47 | <th concordion:assertEquals="#name">Name</th> 48 | </tr> 49 | </thead> 50 | <tbody> 51 | <tr> 52 | <td class="success">John</td> 53 | </tr> 54 | <tr class="surplus"> _ 55 | <td class="failure"><del class="expected"> </del> 56 | <ins class="actual">Paul</ins></td> _ 57 | </tr> _ 58 | </tbody> 59 | </table> 60 |61 | 62 |
Notice how the surplus row is added into the tbody
section.
11 | When used on a <table>, the verifyRows
12 | command compares the contents of the table with the contents of a collection,
13 | and reports similarities, differences and missing or surplus rows.
14 |
17 | The expression must return something Iterable (e.g. a Collection) 18 | and the contents of the table must be in the same order as the collection. 19 |
20 | 21 |26 | <table concordion:verifyRows="#username : usernames()"> 27 | <tr> 28 | <th concordion:assertEquals="#username">Username</th> 29 | </tr> 30 | <tr> 31 | <td>bpeep</td> 32 | </tr> 33 | <tr> 34 | <td>jspratt</td> 35 | </tr> 36 | </table> 37 |38 | 39 |
usernames Collection | 42 |Results in Rows Marked As | 43 |
---|---|
bpeep, jspratt |
46 | SUCCESS, SUCCESS | 47 |
jspratt, bpeep |
50 | FAILURE, FAILURE | 51 |
hdumpty, jspratt |
54 | FAILURE, SUCCESS | 55 |
bpeep |
58 | SUCCESS, MISSING | 59 |
bpeep, jspratt, ppan |
62 | SUCCESS, SUCCESS, SURPLUS | 63 |
rhood, jspratt, ppan, mdaw |
66 | FAILURE, SUCCESS, SURPLUS, SURPLUS | 67 |
10 | Missing rows are marked with CSS class="missing"
11 | on the <tr>
element.
12 |
15 | In the example, below, we will also demonstrate how the verifyRows
16 | command can be used to check multiple properties of objects in a collection.
17 |
Given a method getPeople()
that returns a Collection containing the following Person
objects:
First Name | 28 |Last Name | 29 |Birth Year | 30 |
---|---|---|
John | 33 |Travolta | 34 |1954 | 35 |
Cliff | 38 |Richard | 39 |1940 | 40 |
Britney | 43 |Spears | 44 |1981 | 45 |
And the following instrumentation:
50 | 51 |52 | <table concordion:verifyRows="#person : getPeople()"> 53 | <tr> 54 | <th concordion:assertEquals="#person.firstName">First Name</th> 55 | <th concordion:assertEquals="#person.lastName">Last Name</th> 56 | <th concordion:assertEquals="#person.birthYear">Birth Year</th> 57 | </tr> 58 | <tr> 59 | <td>John</td> 60 | <td>Travolta</td> 61 | <td>1066</td> 62 | </tr> 63 | <tr> 64 | <td>Michael</td> 65 | <td>Jackson</td> 66 | <td>1958</td> 67 | </tr> 68 | <tr> 69 | <td>Britney</td> 70 | <td>Spears</td> 71 | <td>1981</td> 72 | </tr> 73 | <tr> 74 | <td>Mick</td> 75 | <td>Jagger</td> 76 | <td>1943</td> 77 | </tr> 78 | </table> 79 |80 | 81 |
Results in this output:
82 | 83 |84 | <table concordion:verifyRows="#person : getPeople()"> 85 | <tr> 86 | <th concordion:assertEquals="#person.firstName">First Name</th> 87 | <th concordion:assertEquals="#person.lastName">Last Name</th> 88 | <th concordion:assertEquals="#person.birthYear">Birth Year</th> 89 | </tr> 90 | <tr> 91 | <td class="success">John</td> 92 | <td class="success">Travolta</td> 93 | <td class="failure"><del class="expected">1066</del> 94 | <ins class="actual">1954</ins></td> 95 | </tr> 96 | <tr> 97 | <td class="failure"><del class="expected">Michael</del> 98 | <ins class="actual">Cliff</ins></td> 99 | <td class="failure"><del class="expected">Jackson</del> 100 | <ins class="actual">Richard</ins></td> 101 | <td class="failure"><del class="expected">1958</del> 102 | <ins class="actual">1940</ins></td> 103 | </tr> 104 | <tr> 105 | <td class="success">Britney</td> 106 | <td class="success">Spears</td> 107 | <td class="success">1981</td> 108 | </tr> 109 | <tr class="missing"> 110 | <td>Mick</td> 111 | <td>Jagger</td> 112 | <td>1943</td> 113 | </tr> 114 | </table> 115 |116 | 117 |
118 | Notice that the Mick Jagger item was expected to be in the collection,
119 | but was not, so the row is marked with class="missing"
.
120 |
10 | If the collection contains more objects than expected, extra rows are
11 | added to the table. These rows are marked with CSS class="surplus"
12 | on the <tr>
element.
13 |
Given a method getPeople()
that returns a Collection containing the following Person
objects:
First Name | 25 |Last Name | 26 |
---|---|
John | 29 |Travolta | 30 |
Cliff | 33 |Richard | 34 |
And the following instrumentation:
39 | 40 |41 | <table concordion:verifyRows="#person : getPeople()"> 42 | <tr> 43 | <th concordion:assertEquals="#person.firstName">First Name</th> 44 | <th concordion:assertEquals="#person.lastName">Last Name</th> 45 | </tr> 46 | <tr> 47 | <td>John</td> 48 | <td>Travolta</td> 49 | </tr> 50 | </table> 51 |52 | 53 |
Results in this output:
54 | 55 |56 | <table concordion:verifyRows="#person : getPeople()"> 57 | <tr> 58 | <th concordion:assertEquals="#person.firstName">First Name</th> 59 | <th concordion:assertEquals="#person.lastName">Last Name</th> 60 | </tr> 61 | <tr> 62 | <td class="success">John</td> 63 | <td class="success">Travolta</td> 64 | </tr> 65 | <tr class="surplus"> _ 66 | <td class="failure"><del class="expected"> </del> 67 | <ins class="actual">Cliff</ins></td> _ 68 | <td class="failure"><del class="expected"> </del> 69 | <ins class="actual">Richard</ins></td> _ 70 | </tr> _ 71 | </table> 72 |73 | 74 |
8 | CSS resources can be added to the Concordion output using an extension. 9 | The CSS can either be: 10 |
An extension with embedded CSS is installed.
20 |When Concordion is run, 21 | the CSS is embedded in the <head> section of the output HTML.
22 |An extension with linked CSS is installed for the CSS file my.css
with the target location /css/my.css
.
When Concordion is run,
29 | the resource /css/my.css
is available in the Concordion output directory,
30 | and a link to the external stylesheet css/my.css
is declared in the output HTML.
8 | Users can add their own commands to Concordion as extensions. User-contributed commands must use their own namespace that must not contain concordion.org
.
9 | Custom commands are automatically wrapped with a class that will notify ThrowableCaughtListeners
of any Throwables
that are thrown by the command.
10 |
An extension is installed that adds the log
command in the http://myorg.org/my/extension
namespace. This command simply logs the element text.
Running a specification containing:
18 |19 | <div xmlns:myext="http://myorg.org/my/extension"> 20 | <p myext:log="">The answer is 42</p> 21 | </div> 22 | 23 |24 | 25 |
logs: 26 |
27 |Output |
---|
The answer is 42 |
8 | A Concordion extension
introduces additional functionality to Concordion. Extensions are able to:
9 |
8 | Extensions can be added to Concordion using an annotation in the fixture class and/or using a system property. 9 |
10 | 11 |Within a fixture class, fields that are annotated with @org.concordion.api.extension.Extension
will be
15 | added to Concordion as extensions.
Fields with this annotation must be public and must implement org.concordion.api.extension.ConcordionExtension
.
Executing the following fixture:
22 |23 | import org.concordion.integration.junit4.ConcordionRunner; 24 | import org.junit.runner.RunWith; 25 | import org.concordion.api.extension.Extension; 26 | import org.concordion.api.extension.ConcordionExtension; 27 | import test.concordion.extension.fake.*; 28 | 29 | @RunWith(ConcordionRunner.class) 30 | public class ExampleFixture { 31 | 32 | @Extension 33 | public ConcordionExtension extension = new FakeExtension1(); 34 | 35 | @Extension 36 | public FakeExtension2 extension2 = new FakeExtension2(); 37 | } 38 |39 |
will install both extensions FakeExtension1, FakeExtension2.
40 |Extensions will be loaded from the fixture class and any of its superclasses in parent-first order. A common pattern is to have the extensions defined in a "base fixture".
43 | 44 | 45 |Executing the following fixture:
48 |49 | import org.concordion.integration.junit4.ConcordionRunner; 50 | import org.junit.runner.RunWith; 51 | import org.concordion.api.extension.Extension; 52 | import org.concordion.api.extension.ConcordionExtension; 53 | import test.concordion.extension.fake.*; 54 | 55 | @RunWith(ConcordionRunner.class) 56 | public class ExampleFixture extends BaseFixture { 57 | @Extension 58 | public ConcordionExtension extension = new FakeExtension1("ExampleExtension"); 59 | 60 | } 61 |62 |
which has superclass
63 |64 | import org.concordion.integration.junit4.ConcordionRunner; 65 | import org.junit.runner.RunWith; 66 | import org.concordion.api.extension.Extension; 67 | import org.concordion.api.extension.ConcordionExtension; 68 | import test.concordion.extension.fake.*; 69 | 70 | @RunWith(ConcordionRunner.class) 71 | public class BaseFixture { 72 | 73 | @Extension 74 | public FakeExtension2 extension2 = new FakeExtension2("SuperExtension"); 75 | } 76 |77 |
will install both the extensions initialised with parameters SuperExtension, ExampleExtension.
78 |As an alternative, extensions that require no state from the fixture can be defined statically on the fixture class with the @org.concordion.api.extension.Extensions
82 | annotation. This annotation is parameterised with a list of the extension, or extension factory, classes to be installed.
85 | Extensions must implement org.concordion.api.extension.ConcordionExtension
.
86 | Extension factories must implement org.concordion.api.extension.ConcordionExtensionFactory
.
87 |
Executing the following fixture:
93 |94 | import org.concordion.integration.junit4.ConcordionRunner; 95 | import org.junit.runner.RunWith; 96 | import org.concordion.api.extension.Extensions; 97 | import org.concordion.api.extension.ConcordionExtension; 98 | import test.concordion.extension.fake.*; 99 | 100 | @RunWith(ConcordionRunner.class) 101 | @Extensions({FakeExtension1.class, FakeExtension2Factory.class}) 102 | public class ExampleFixture { 103 | 104 | } 105 |106 |
will install both extensions FakeExtension1, FakeExtension2FromFactory.
107 |Extensions will be loaded from the fixture class and any of its superclasses in parent-first order. A common pattern is to have the extensions defined in a "base fixture".
110 | 111 | 112 |Executing the following fixture:
115 |116 | import org.junit.runner.RunWith; 117 | import org.concordion.integration.junit4.ConcordionRunner; 118 | import org.concordion.api.extension.Extensions; 119 | import org.concordion.api.extension.ConcordionExtension; 120 | import test.concordion.extension.fake.*; 121 | 122 | @RunWith(ConcordionRunner.class) 123 | public class ExampleFixture extends BaseFixture { 124 | 125 | } 126 |127 | 128 |
which has superclass
129 |130 | import org.junit.runner.RunWith; 131 | import org.concordion.integration.junit4.ConcordionRunner; 132 | import org.concordion.api.extension.Extensions; 133 | import org.concordion.api.extension.ConcordionExtension; 134 | import test.concordion.extension.fake.*; 135 | 136 | @RunWith(ConcordionRunner.class) 137 | @Extensions({FakeExtension1.class, FakeExtension2.class}) 138 | public class BaseFixture { 139 | 140 | } 141 |142 |
will install both extensions FakeExtension1, FakeExtension2.
143 |Executing the following fixture:
150 |151 | import org.junit.runner.RunWith; 152 | import org.concordion.integration.junit4.ConcordionRunner; 153 | import org.concordion.api.extension.Extensions; 154 | import org.concordion.api.extension.ConcordionExtension; 155 | import test.concordion.extension.fake.*; 156 | 157 | @RunWith(ConcordionRunner.class) 158 | @Extensions({FakeExtension2.class}) 159 | public class ExampleFixture extends BaseFixture { 160 | 161 | } 162 |163 | 164 |
which has superclass
165 |166 | import org.junit.runner.RunWith; 167 | import org.concordion.integration.junit4.ConcordionRunner; 168 | import org.concordion.api.extension.Extensions; 169 | import org.concordion.api.extension.ConcordionExtension; 170 | import test.concordion.extension.fake.*; 171 | 172 | @RunWith(ConcordionRunner.class) 173 | @Extensions({FakeExtension1.class}) 174 | public class BaseFixture { 175 | 176 | } 177 |178 |
will install both extensions FakeExtension1, FakeExtension2.
179 |Alternatively, extensions can be specified by setting a system property. This can be useful if the extensions need to be configured independently from the fixtures.
185 |Set the system property concordion.extensions
to a comma-separated list containing:
191 | All extensions and/or extension factories must be present on the classpath.
192 | Extensions must implement org.concordion.api.extension.ConcordionExtension
.
193 | Extension factories must implement org.concordion.api.extension.ConcordionExtensionFactory
.
194 |
Given the system property concordion.extensions
is set to "test.concordion.extension.fake.FakeExtension1, test.concordion.extension.fake.FakeExtension2Factory
",
Concordion fixtures will be run with both extensions FakeExtension1, FakeExtension2FromFactory.
203 |8 | JavaScript resources can be added to the Concordion output using an extension. 9 | The JavaScript can either be: 10 |
An extension with embedded JavaScript is installed.
20 |When Concordion is run, 21 | the JavaScript is embedded in the <head> section of the output HTML.
22 |An extension with linked JavaScript is installed for the JavaScript file my.js
with the target location /js/my.js
.
When Concordion is run,
29 | the resource /js/my.js
is available in the Concordion output directory,
30 | and a link to the external stylesheet js/my.js
is declared in the output HTML.
8 | Resources can be added to the Concordion output folder using an extension. 9 |
10 | 11 |13 | "Static" resources can be created in the Concordion output folder. These resources must be available prior to running the Concordion specification. 14 |
15 | 16 |An extension is installed that copies test/concordion/o.png
from the classpath to /images/o.png
.
When Concordion is run,
22 | the resource /images/o.png
is available in the Concordion output directory.
27 | "Dynamic" resources can be written to the Concordion output folder during a Concordion run. These resources are typically created while running the Concordion specification, 28 | for example screenshots being generated by an assertEqualsListener. 29 |
30 |An extension is installed that creates the dynamic resource /resource/my.txt
in the target.
When Concordion is run,
35 | the resource /resource/my.txt
is available in the Concordion output directory.
8 | When executing tables, Concordion notifies listeners of execute events for each row of the table. 9 | Execute listeners are notified that execution is complete prior to the assertions being evaluated. 10 |
11 | 12 | 13 |An extension is installed that logs assertEquals
and execute
commands. When the following instrumentation:
19 | <table concordion:execute="#username = generateUsername(#fullName)"> 20 | <tr> 21 | <th concordion:set="#fullName">Full Name</th> 22 | <th concordion:assertEquals="#username">Username</th> 23 | </tr> 24 | <tr> 25 | <td>Fred Bloggs</td> 26 | <td>fredbloggs</td> 27 | </tr> 28 | <tr> 29 | <td>John Doe</td> 30 | <td>johndoe</td> 31 | </tr> 32 | <tr> 33 | <td>Winston Churchill</td> 34 | <td>winston</td> 35 | </tr> 36 | </table> 37 |38 | 39 |
is run with a method generateUsername()
that returns the
40 | full name in lowercase with spaces removed, the logged events are:
41 |
Event |
---|
Execute 'Fred Bloggs, fredbloggs' |
Success 'fredbloggs' |
Execute 'John Doe, johndoe' |
Success 'johndoe' |
Execute 'Winston Churchill, winston' |
Failure expected:'winston' actual:'winstonchurchill' |
8 | The execution of Concordion commands can be observed using listeners
. Listeners have access to the HTML element with which the command is associated,
9 | and can modify the output HTML.
10 |
An extension is installed that listens to assertEquals, assertTrue, assertFalse and execute commands. When the following instrumentation:
17 |18 | <p concordion:execute="#result=sqrt(#num)">The square root of <span concordion:set="#num">4.0</span> is <span concordion:assertEquals="#result">2.0</span></p> 19 | 20 | <p concordion:execute="#result=sqrt(#num)">The square root of <span concordion:set="#num">16.0</span> is <span concordion:assertEquals="#result">3.0</span></p> 21 | 22 | <p><span concordion:set="#num">3</span> 23 | is <span concordion:assertTrue="isPositive(#num)">is positive</span> 24 | </p> 25 | 26 | <p><span concordion:set="#num">-4</span> 27 | is <span concordion:assertTrue="isPositive(#num)">is positive</span> 28 | </p> 29 | 30 | <p><span concordion:set="#num">-5</span> 31 | is <span concordion:assertFalse="isPositive(#num)">is not positive</span> 32 | </p> 33 | 34 | <p><span concordion:set="#num">6</span> 35 | is <span concordion:assertFalse="isPositive(#num)">is not positive</span> 36 | </p> 37 | 38 |39 | 40 |
is run with a fixture that performs the arithmetical operations, the logged events are: 41 |
42 |Event |
---|
Execute 'The square root of 4.0 is 2.0' |
Success '2.0' |
Execute 'The square root of 16.0 is 3.0' |
Failure expected:'3.0' actual:'4.0' |
Success 'is positive' |
Failure expected:'is positive' actual:'== false' |
Success 'is not positive' |
Failure expected:'is not positive' actual:'== true' |
Run listeners
are notified of the outcome of run commands (success, failure or ignored)Throwable caught listeners
are notified of any throwables
that occur while processing a command, including custom commands.Concordion build listeners
provide access to the target
to allow resources to be writtenDocument parsing listeners
provide access to the document
prior to parsingSpecification processing listeners
provide access to the specification
before and after processing8 | When verifying rows, Concordion notifies listeners when the expression has been evaluated and also of each missing row or surplus row in the collection returned by the expression. 9 |
10 | 11 | 12 |An extension is installed that logs assertEquals
and verifyRows
commands. When the following instrumentation:
18 | <table concordion:verifyRows="#beatle : getGeorgeAndRingo()"> 19 | <tr><th concordion:assertEquals="#beatle">Matching Beatle</th></tr> 20 | <tr><td>George Harrison</td></tr> 21 | <tr><td>Dingo Starr</td></tr> 22 | <tr><td>George Michael</td></tr> 23 | </table> 24 |25 | 26 |
is run with a method getGeorgeAndRingo()
that returns George Harrison
and Ringo Starr
, the logged events are:
Event |
---|
Evaluated '#beatle : getGeorgeAndRingo()' |
Success 'George Harrison' |
Failure expected:'Dingo Starr' actual:'Ringo Starr' |
Missing Row 'George Michael' |
8 | After processing a document, Concordion outputs HTML based on the 9 | input document with the results marked up. 10 |
11 | 12 |
15 | The assertEquals
command reports
16 | successes and
17 | failures.
18 |
21 | The assertTrue
command reports
22 | successes and
23 | failures.
24 |
27 | The verifyRows
command reports
28 | missing and
29 | surplus rows.
30 |
8 | Anchor elements (<a>) have to be treated slightly differently to 9 | other elements to prevent the 'actual' value becoming a link. Rather than 10 | nesting <span> elements inside the <a>, we nest the <a> 11 | inside a new 'failure' <span>. 12 |
13 | 14 |<a href="abc.html">ABC</a>19 | 20 |
When marked as a failure, with actual value XYZ
, it becomes:
23 | <span class="failure"> _ 24 | <span class="expected"> _ 25 | <a href="abc.html">ABC</a> _ 26 | </span> _ 27 | <span class="actual">XYZ</span> _ 28 | </div> 29 |30 |
8 | If the element is empty, then a non-breaking space ( ) 9 | is inserted (so that the failure shows up when displayed in a browser). 10 |
11 | 12 |<div concordion:assertEquals="acronym" />17 | 18 |
19 | When marked as a failure, with actual value
20 | XYZ
, it becomes:
21 |
24 | <div concordion:assertEquals="acronym" class="failure"> _ 25 | <del class="expected"> </del> 26 | <ins class="actual">XYZ</ins> _ 27 | </div> 28 |29 |
<div concordion:assertEquals="acronym">ABC</div>36 | 37 |
When marked as a failure, with a blank actual value, it becomes:
38 | 39 |40 | <div concordion:assertEquals="acronym" class="failure"> _ 41 | <del class="expected">ABC</del> 42 | <ins class="actual"> </ins> _ 43 | </div> 44 |45 |
11 | Failures are indicated by adding a class="failure"
attribute to the element,
12 | and replacing the contents with a <del>
and <ins>
13 | elements containing the expected value and the actual value respectively.
14 |
<p concordion:assertEquals="acronym">ABC</p>21 | 22 |
When marked as a failure, with acroynm
23 | returning XYZ
, it becomes:
26 | <p concordion:assertEquals="acronym" class="failure"> _ 27 | <del class="expected">ABC</del> 28 | <ins class="actual">XYZ</ins> _ 29 | </p> 30 |31 |
Note: The underscores indicate line continuations for readability only. They are not output. In reality, it is all in one long line.
32 | 33 |8 | Nested elements are moved into the 'expected' span. 9 |
10 | 11 |<div concordion:assertEquals="acronym">My <em>simple</em> example</div>16 | 17 |
When marked as a failure, with actual value XYZ
, it becomes:
20 | <div concordion:assertEquals="acronym" class="failure"> _ 21 | <del class="expected">My <em>simple</em> example</del> 22 | <ins class="actual">XYZ</ins> _ 23 | </div> 24 |25 |
8 | If the element is empty then a non-breaking space ( ) is inserted so 9 | that there is something there to show the success. 10 |
11 | 12 |<span concordion:assertEquals="username"/>17 | 18 |
When marked as a success becomes:
19 | 20 |<span concordion:assertEquals="username" class="success"> </span>21 |
8 | If an element already has attributes then these attributes will be 9 | retained. 10 |
11 | 12 |<span id="example" concordion:assertEquals="username">fred</span>17 | 18 |
When marked as a success becomes:
19 | 20 |<span id="example" concordion:assertEquals="username" class="success">fred</span>21 |
8 | If an element already has a class attribute then the new attribute will 9 | be appended to the existing value, separated by space. 10 |
11 | 12 |<span concordion:assertEquals="username" class="blah">fred</span>17 | 18 |
When marked as a success becomes:
19 | 20 |<span concordion:assertEquals="username" class="blah success">fred</span>21 |
8 | Successes are indicated by adding a class="success"
attribute to the element.
9 | Typically this will result in the element being displayed in green (depending on the CSS stylesheet).
10 |
17 | <span concordion:assertEquals="username">fred</span> 18 |19 | 20 |
When marked as a success becomes:
21 | 22 |23 | <span concordion:assertEquals="username" class="success">fred</span> 24 |25 |
11 | Failures are indicated by adding a class="failure"
attribute to the element,
12 | and replacing the contents with a <del>
and <ins>
13 | elements containing the expected value and the actual value respectively.
14 |
21 | The output of running this: 22 |
23 | 24 |<p concordion:assertTrue="isPalindrome(#TEXT)">ABB</p>25 | 26 |
Looks like this:
27 | 28 |29 | <p concordion:assertTrue="isPalindrome(#TEXT)" class="failure"> _ 30 | <del class="expected">ABB</del> 31 | <ins class="actual">== false</ins> _ 32 | </p> 33 |34 |
Note: The underscores indicate line continuations for readability only. They are not output. In reality, it is all in one long line.
35 | 36 |
11 | Successes are indicated by adding a class="success"
attribute to the element.
12 |
19 | The output of running this: 20 |
21 | 22 |<p concordion:assertTrue="isPalindrome(#TEXT)">ABBA</p>23 | 24 |
Looks like this:
25 | 26 |27 | <p concordion:assertTrue="isPalindrome(#TEXT)" class="success">ABBA</p> 28 |29 | 30 |
8 | After a user logs into the system, a greeting is 9 | displayed saying "Hello [user's first name]!" 10 |
11 | 12 |17 | When user Bob 18 | logs in, the greeting will be: 19 | Hello Bob! 20 |
21 | 22 | 23 |8 | Username searches return partial matches, i.e. all usernames containing 9 | the search string are returned. 10 |
11 | 12 |Given these users:
17 | 18 |Username | 21 |
---|
john.lennon | 24 |
ringo.starr | 27 |
george.harrison | 30 |
paul.mcartney | 33 |
Searching for "arr" will return:
37 | 38 |Matching Usernames | 41 |
---|
george.harrison | 44 |
ringo.starr | 47 |
11 | The greeting Hello David! 12 | should be displayed for David 13 | when he logs in. 14 |
15 | 16 |Doing something
17 | 18 |First Name | 21 |Last Name | 22 |
---|---|
John | 25 |Travolta | 26 |
Frank | 29 |Zappa | 30 |