├── report └── .gitignore ├── .gitignore ├── behat.yml ├── composer.json ├── src ├── Classes │ ├── Suite.php │ ├── Scenario.php │ ├── Step.php │ └── Feature.php ├── Context │ └── ScreenshotContext.php ├── BehatHTMLFormatterExtension.php ├── Renderer │ ├── RendererInterface.php │ ├── TwigRenderer.php │ ├── MinimalRenderer.php │ ├── BaseRenderer.php │ └── Behat2Renderer.php ├── Printer │ └── FileOutputPrinter.php └── Formatter │ └── BehatHTMLFormatter.php ├── LICENSE ├── assets └── Twig │ └── css │ ├── callout.css │ ├── callout.less │ ├── style.less │ └── style.css ├── .travis.yml ├── features ├── twig_renderer.feature └── bootstrap │ └── FeatureContext.php ├── templates ├── index.backup.html.twig └── index.html.twig └── README.md /report/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | build/ 4 | .idea 5 | -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | formatters: 3 | html: 4 | output_path: %paths.base%/report 5 | pretty: ~ 6 | 7 | extensions: 8 | emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension: 9 | name: html 10 | renderer: Twig 11 | file_name: index 12 | print_args: true 13 | print_outp: true 14 | loop_break: true 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emuse/behat-html-formatter", 3 | "description": "This will create a html formatter for Behat.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Neal Vanmeert", 8 | "email": "neal@emuse.be" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.3.0", 13 | "twig/twig":"~1.5|~2.0", 14 | "behat/behat": "~3.0", 15 | "behat/gherkin": "~4.2" 16 | }, 17 | "require-dev": { 18 | "symfony/process": ">2.3,<4.0", 19 | "phpunit/phpunit": "~4.1" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "emuse\\BehatHTMLFormatter\\": "src/" 24 | } 25 | }, 26 | "extra": { 27 | "branch-alias": { 28 | "dev-master": "0.1.x-dev" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Classes/Suite.php: -------------------------------------------------------------------------------- 1 | name; 16 | } 17 | 18 | /** 19 | * @param mixed $name 20 | */ 21 | public function setName($name) 22 | { 23 | $this->name = $name; 24 | } 25 | 26 | /** 27 | * @return mixed 28 | */ 29 | public function getFeatures() 30 | { 31 | return $this->features; 32 | } 33 | 34 | /** 35 | * @param mixed $features 36 | */ 37 | public function setFeatures($features) 38 | { 39 | $this->features = $features; 40 | } 41 | 42 | public function addFeature($feature) 43 | { 44 | $this->features[] = $feature; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Neal Vanmeert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /assets/Twig/css/callout.css: -------------------------------------------------------------------------------- 1 | .bs-callout { 2 | padding: 20px; 3 | margin: 20px 0; 4 | border: 1px solid #eee; 5 | border-left-width: 5px; 6 | border-radius: 3px; 7 | } 8 | .bs-callout h4 { 9 | margin-top: 0; 10 | margin-bottom: 5px; 11 | } 12 | .bs-callout p:last-child { 13 | margin-bottom: 0; 14 | } 15 | .bs-callout code { 16 | border-radius: 3px; 17 | } 18 | .bs-callout + .bs-callout { 19 | margin-top: -5px; 20 | } 21 | .bs-callout-default { 22 | border-left-color: #777; 23 | } 24 | .bs-callout-default h4 { 25 | color: #777; 26 | } 27 | .bs-callout-primary { 28 | border-left-color: #428bca; 29 | } 30 | .bs-callout-primary h4 { 31 | color: #428bca; 32 | } 33 | .bs-callout-success { 34 | border-left-color: #5cb85c; 35 | } 36 | .bs-callout-success h4 { 37 | color: #5cb85c; 38 | } 39 | .bs-callout-danger { 40 | border-left-color: #d9534f; 41 | } 42 | .bs-callout-danger h4 { 43 | color: #d9534f; 44 | } 45 | .bs-callout-warning { 46 | border-left-color: #f0ad4e; 47 | } 48 | .bs-callout-warning h4 { 49 | color: #f0ad4e; 50 | } 51 | .bs-callout-info { 52 | border-left-color: #5bc0de; 53 | } 54 | .bs-callout-info h4 { 55 | color: #5bc0de; 56 | } 57 | -------------------------------------------------------------------------------- /assets/Twig/css/callout.less: -------------------------------------------------------------------------------- 1 | .bs-callout { 2 | padding: 20px; 3 | margin: 20px 0; 4 | border: 1px solid #eee; 5 | border-left-width: 5px; 6 | border-radius: 3px; 7 | } 8 | .bs-callout h4 { 9 | margin-top: 0; 10 | margin-bottom: 5px; 11 | } 12 | .bs-callout p:last-child { 13 | margin-bottom: 0; 14 | } 15 | .bs-callout code { 16 | border-radius: 3px; 17 | } 18 | .bs-callout+.bs-callout { 19 | margin-top: -5px; 20 | } 21 | .bs-callout-default { 22 | border-left-color: #777; 23 | } 24 | .bs-callout-default h4 { 25 | color: #777; 26 | } 27 | .bs-callout-primary { 28 | border-left-color: #428bca; 29 | } 30 | .bs-callout-primary h4 { 31 | color: #428bca; 32 | } 33 | .bs-callout-success { 34 | border-left-color: #5cb85c; 35 | } 36 | .bs-callout-success h4 { 37 | color: #5cb85c; 38 | } 39 | .bs-callout-danger { 40 | border-left-color: #d9534f; 41 | } 42 | .bs-callout-danger h4 { 43 | color: #d9534f; 44 | } 45 | .bs-callout-warning { 46 | border-left-color: #f0ad4e; 47 | } 48 | .bs-callout-warning h4 { 49 | color: #f0ad4e; 50 | } 51 | .bs-callout-info { 52 | border-left-color: #5bc0de; 53 | } 54 | .bs-callout-info h4 { 55 | color: #5bc0de; 56 | } 57 | -------------------------------------------------------------------------------- /src/Context/ScreenshotContext.php: -------------------------------------------------------------------------------- 1 | screenshotDir = $screenshotDir; 15 | } 16 | 17 | /** 18 | * @BeforeScenario 19 | * 20 | * @param BeforeScenarioScope $scope 21 | */ 22 | public function setUpTestEnvironment($scope) 23 | { 24 | $this->currentScenario = $scope->getScenario(); 25 | } 26 | 27 | /** 28 | * @AfterStep 29 | * 30 | * @param AfterStepScope $scope 31 | */ 32 | public function afterStep($scope) 33 | { 34 | // if test is passed, skip taking screenshot 35 | if ($scope->getTestResult()->isPassed()) { 36 | return; 37 | } 38 | 39 | // create filename string 40 | $featureFolder = preg_replace('/\W/', '', $scope->getFeature()->getTitle()); 41 | 42 | $scenarioName = $this->currentScenario->getTitle(); 43 | $fileName = preg_replace('/\W/', '', $scenarioName).'.png'; 44 | 45 | // create screenshots directory if it doesn't exist 46 | if (!file_exists($this->screenshotDir.'/'.$featureFolder)) { 47 | mkdir($this->screenshotDir.'/'.$featureFolder, 0777, true); 48 | } 49 | 50 | $this->saveScreenshot($fileName, $this->screenshotDir.'/'.$featureFolder.'/'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer 8 | 9 | php: 10 | - 5.3 11 | - 5.4 12 | - 5.5 13 | - 5.6 14 | - 7.0 15 | - nightly 16 | - hhvm 17 | 18 | env: 19 | - DEPS='low' 20 | - DEPS='dev' 21 | - DEPS='normal' 22 | 23 | matrix: 24 | fast_finish: true 25 | allow_failures: 26 | - env: DEPS='dev' 27 | - php: nightly 28 | 29 | before_install: 30 | - if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then cat $HOME/.phpenv/versions/$TRAVIS_PHP_VERSION/etc/conf.d/xdebug.ini > ./xdebug.ini ; fi 31 | - if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then phpenv config-rm xdebug.ini ; fi 32 | - if [ "$DEPS" == "normal" ] && [ "$TRAVIS_PHP_VERSION" == "5.6" ] ; then export COVERALLS=true; fi 33 | - if [ "$COVERALLS" == "true" ] ; then echo "Will try to generate coveralls"; fi 34 | - echo "CRONTAB before tests" 35 | - echo "`crontab -l`" 36 | - echo "Using in php version $TRAVIS_PHP_VERSION" 37 | - echo "Preparing blackfire" 38 | - travis_retry composer selfupdate 39 | - echo '#!/bin/bash' > install.sh 40 | - echo -n "composer update" >> install.sh 41 | - echo -n " --prefer-dist" >> install.sh 42 | - if [ "$DEPS" == "low" ]; then echo -n " --prefer-lowest" >> install.sh; fi; 43 | - if [ "$DEPS" == "normal" ]; then echo -n " --prefer-stable" >> install.sh; fi; 44 | - sed -n '/prefer-stable/!p' composer.json > tmp.json && mv tmp.json composer.json; 45 | 46 | install: 47 | - travis_retry composer global require kherge/box --prefer-dist 48 | - travis_retry composer global require phing/phing --prefer-dist 49 | - travis_retry composer global require satooshi/php-coveralls --prefer-dist 50 | - $HOME/.composer/vendor/bin/box --version 51 | - $HOME/.composer/vendor/bin/phing -v 52 | - export COMPOSER_ROOT_VERSION=dev-master 53 | - travis_retry /bin/bash ./install.sh 54 | 55 | before_script: 56 | - echo "= 50400) echo ',@php5.4'; if (PHP_VERSION_ID >= 70000) echo ',@php7'; }" > php_version_tags.php 57 | - if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then phpenv config-add xdebug.ini ; fi 58 | 59 | script: 60 | - ./vendor/bin/behat 61 | -------------------------------------------------------------------------------- /src/BehatHTMLFormatterExtension.php: -------------------------------------------------------------------------------- 1 | children()->scalarNode('name')->defaultValue('emusehtml'); 59 | $builder->children()->scalarNode('renderer')->defaultValue('Twig'); 60 | $builder->children()->scalarNode('file_name')->defaultValue('generated'); 61 | $builder->children()->scalarNode('print_args')->defaultValue('false'); 62 | $builder->children()->scalarNode('print_outp')->defaultValue('false'); 63 | $builder->children()->scalarNode('loop_break')->defaultValue('false'); 64 | } 65 | 66 | /** 67 | * Loads extension services into temporary container. 68 | * 69 | * @param ContainerBuilder $container 70 | * @param array $config 71 | */ 72 | public function load(ContainerBuilder $container, array $config) 73 | { 74 | $definition = new Definition('emuse\\BehatHTMLFormatter\\Formatter\\BehatHTMLFormatter'); 75 | $definition->addArgument($config['name']); 76 | $definition->addArgument($config['renderer']); 77 | $definition->addArgument($config['file_name']); 78 | $definition->addArgument($config['print_args']); 79 | $definition->addArgument($config['print_outp']); 80 | $definition->addArgument($config['loop_break']); 81 | 82 | $definition->addArgument('%paths.base%'); 83 | $container->setDefinition('html.formatter', $definition) 84 | ->addTag('output.formatter'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Renderer/RendererInterface.php: -------------------------------------------------------------------------------- 1 | .panel-heading { 126 | color: white; 127 | background-color: @color-passed; 128 | background-image: none; 129 | } 130 | } 131 | &.pending { 132 | > .panel-heading { 133 | color: white; 134 | background-color: @color-pending; 135 | background-image: none; 136 | } 137 | } 138 | &.failed { 139 | > .panel-heading { 140 | color: white; 141 | background-color: @color-failed; 142 | background-image: none; 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /features/twig_renderer.feature: -------------------------------------------------------------------------------- 1 | Feature: Twig Renderer 2 | 3 | Background: 4 | Given a file named "behat.yml" with: 5 | """ 6 | default: 7 | formatters: 8 | html: 9 | output_path: %paths.base%/build 10 | extensions: 11 | emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension: 12 | name: html 13 | renderer: Twig 14 | file_name: Index 15 | print_args: true 16 | print_outp: true 17 | loop_break: true 18 | suites: 19 | suite1: 20 | paths: [ "%paths.base%/features/suite1" ] 21 | suite2: 22 | paths: [ "%paths.base%/features/suite2" ] 23 | suite3: 24 | paths: [ "%paths.base%/features/suite3" ] 25 | """ 26 | Given a file named "features/bootstrap/FeatureContext.php" with: 27 | """ 28 | .panel-heading { 153 | color: white; 154 | background-color: #00a65a; 155 | background-image: none; 156 | } 157 | #scenario-overview .feature .details .panel.pending > .panel-heading { 158 | color: white; 159 | background-color: #e38d13; 160 | background-image: none; 161 | } 162 | #scenario-overview .feature .details .panel.failed > .panel-heading { 163 | color: white; 164 | background-color: #f56956; 165 | background-image: none; 166 | } 167 | .list-group-item.break { 168 | padding: 1px; 169 | background-color: #808080; 170 | } -------------------------------------------------------------------------------- /src/Classes/Scenario.php: -------------------------------------------------------------------------------- 1 | name; 39 | } 40 | 41 | /** 42 | * @param mixed $name 43 | */ 44 | public function setName($name) 45 | { 46 | $this->name = $name; 47 | } 48 | 49 | public function getScreenshotName() 50 | { 51 | return $this->screenshotName; 52 | } 53 | 54 | public function setScreenshotName($scenarioName) 55 | { 56 | $this->screenshotName = preg_replace('/\W/', '', $scenarioName).'.png'; 57 | } 58 | 59 | /** 60 | * @return int 61 | */ 62 | public function getLoopCount() 63 | { 64 | return $this->loopCount; 65 | } 66 | 67 | /** 68 | * @param int $loopCount 69 | */ 70 | public function setLoopCount($loopCount) 71 | { 72 | $this->loopCount = $loopCount; 73 | } 74 | 75 | /** 76 | * @return mixed 77 | */ 78 | public function getLine() 79 | { 80 | return $this->line; 81 | } 82 | 83 | /** 84 | * @param mixed $line 85 | */ 86 | public function setLine($line) 87 | { 88 | $this->line = $line; 89 | } 90 | 91 | /** 92 | * @return mixed 93 | */ 94 | public function getTags() 95 | { 96 | return $this->tags; 97 | } 98 | 99 | /** 100 | * @param mixed $tags 101 | */ 102 | public function setTags($tags) 103 | { 104 | $this->tags = $tags; 105 | } 106 | 107 | /** 108 | * @return bool 109 | */ 110 | public function isPassed() 111 | { 112 | return $this->passed; 113 | } 114 | 115 | /** 116 | * @param bool $passed 117 | */ 118 | public function setPassed($passed) 119 | { 120 | $this->passed = $passed; 121 | } 122 | 123 | /** 124 | * @return bool 125 | */ 126 | public function isPending() 127 | { 128 | return $this->pending; 129 | } 130 | 131 | /** 132 | * @param bool $pending 133 | */ 134 | public function setPending($pending) 135 | { 136 | $this->pending = $pending; 137 | } 138 | 139 | /** 140 | * @return Step[] 141 | */ 142 | public function getSteps() 143 | { 144 | return $this->steps; 145 | } 146 | 147 | /** 148 | * @param Step[] $steps 149 | */ 150 | public function setSteps($steps) 151 | { 152 | $this->steps = $steps; 153 | } 154 | 155 | /** 156 | * @param Step $step 157 | */ 158 | public function addStep($step) 159 | { 160 | $this->steps[] = $step; 161 | } 162 | 163 | /** 164 | * @return int 165 | */ 166 | public function getId() 167 | { 168 | return $this->id; 169 | } 170 | 171 | /** 172 | * @param int $id 173 | */ 174 | public function setId($id) 175 | { 176 | $this->id = $id; 177 | } 178 | 179 | public function getLoopSize() 180 | { 181 | //behat 182 | return $this->loopCount > 0 ? sizeof($this->steps) / $this->loopCount : sizeof($this->steps); 183 | } 184 | 185 | public function setScreenshotPath($string) 186 | { 187 | $this->screenshotPath = $string; 188 | } 189 | 190 | /** 191 | * @return mixed 192 | */ 193 | public function getScreenshotPath() 194 | { 195 | return $this->screenshotPath; 196 | } 197 | 198 | /** 199 | * Gets relative path for screenshot. 200 | * 201 | * @return bool|string 202 | */ 203 | public function getRelativeScreenshotPath() 204 | { 205 | if (!isset($this->screenshotPath) || !file_exists($this->screenshotPath)) { 206 | return false; 207 | } 208 | 209 | return '.'.substr($this->screenshotPath, strpos($this->screenshotPath, '/assets/screenshots')); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Classes/Step.php: -------------------------------------------------------------------------------- 1 | keyword; 26 | } 27 | 28 | /** 29 | * @param mixed $keyword 30 | */ 31 | public function setKeyword($keyword) 32 | { 33 | $this->keyword = $keyword; 34 | } 35 | 36 | /** 37 | * @return mixed 38 | */ 39 | public function getText() 40 | { 41 | return $this->text; 42 | } 43 | 44 | /** 45 | * @param mixed $text 46 | */ 47 | public function setText($text) 48 | { 49 | $this->text = $text; 50 | } 51 | 52 | /** 53 | * @return mixed 54 | */ 55 | public function getArgumentType() 56 | { 57 | return $this->argumentType; 58 | } 59 | 60 | /** 61 | * @param mixed $arguments 62 | */ 63 | public function setArgumentType($argumentType) 64 | { 65 | $this->argumentType = $argumentType; 66 | } 67 | 68 | /** 69 | * @return mixed 70 | */ 71 | public function getArguments() 72 | { 73 | return $this->arguments; 74 | } 75 | 76 | /** 77 | * @param mixed $arguments 78 | */ 79 | public function setArguments($arguments) 80 | { 81 | $this->arguments = $arguments; 82 | } 83 | 84 | /** 85 | * @return mixed 86 | */ 87 | public function getLine() 88 | { 89 | return $this->line; 90 | } 91 | 92 | /** 93 | * @param mixed $line 94 | */ 95 | public function setLine($line) 96 | { 97 | $this->line = $line; 98 | } 99 | 100 | /** 101 | * @return mixed 102 | */ 103 | public function getResult() 104 | { 105 | return $this->result; 106 | } 107 | 108 | /** 109 | * @param mixed $result 110 | */ 111 | public function setResult($result) 112 | { 113 | $this->result = $result; 114 | } 115 | 116 | /** 117 | * @return mixed 118 | */ 119 | public function getException() 120 | { 121 | return $this->exception; 122 | } 123 | 124 | /** 125 | * @param mixed $exception 126 | */ 127 | public function setException($exception) 128 | { 129 | $this->exception = $exception; 130 | } 131 | 132 | /** 133 | * @return mixed 134 | */ 135 | public function getDefinition() 136 | { 137 | return $this->definition; 138 | } 139 | 140 | /** 141 | * @param mixed $definition 142 | */ 143 | public function setDefinition($definition) 144 | { 145 | $this->definition = $definition; 146 | } 147 | 148 | /** 149 | * @return mixed 150 | */ 151 | public function getOutput() 152 | { 153 | return $this->output; 154 | } 155 | 156 | /** 157 | * @param mixed $output 158 | */ 159 | public function setOutput($output) 160 | { 161 | $this->output = $output; 162 | } 163 | 164 | /** 165 | * @return mixed 166 | */ 167 | public function getResultCode() 168 | { 169 | return $this->resultCode; 170 | } 171 | 172 | /** 173 | * @param mixed $resultCode 174 | */ 175 | public function setResultCode($resultCode) 176 | { 177 | $this->resultCode = $resultCode; 178 | } 179 | 180 | /** 181 | * @return bool 182 | */ 183 | public function isPassed() 184 | { 185 | return StepResult::PASSED == $this->resultCode; 186 | } 187 | 188 | /** 189 | * @return bool 190 | */ 191 | public function isSkipped() 192 | { 193 | return StepResult::SKIPPED == $this->resultCode; 194 | } 195 | 196 | /** 197 | * @return bool 198 | */ 199 | public function isPending() 200 | { 201 | return StepResult::PENDING == $this->resultCode || StepResult::UNDEFINED == $this->resultCode; 202 | } 203 | 204 | /** 205 | * @return bool 206 | */ 207 | public function isFailed() 208 | { 209 | return StepResult::FAILED == $this->resultCode; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /templates/index.backup.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Behat report 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 46 | 47 |
48 |
49 |
50 | {{ failedScenarios|length }} scenarios failed of {{ (failedScenarios|length) + (passedScenarios|length) }} 51 | scenarios 52 |
53 |
54 | {{ failedSteps|length }} steps failed of {{ (failedSteps|length) + (passedSteps|length) }} 55 |
56 |
57 |
58 | {% for suite in suites %} 59 |
60 |
61 |

{% trans %}Suite: {% endtrans %}{{ suite.name }}

62 | {% for feature in suite.features %} 63 |

{% trans %}Feature: {% endtrans %}{{ feature.name }}

64 | {% for tag in feature.tags %} 65 | {{ tag }} 66 | {% endfor %} 67 |

{{ feature.description|raw|nl2br }}

68 | {% for scenario in feature.scenarios %} 69 |
70 |
71 |

{% trans %}Scenario: {% endtrans %}{{ scenario.name }}

72 | {% for tag in scenario.tags %} 73 | {{ tag }} 74 | {% endfor %} 75 |
76 |
{{ feature.file }}: {{ scenario.line }}
77 | {% for step in scenario.steps %} 78 |
79 |
80 | {{ step.keyword }} {{ step.text }} 81 |
82 |
83 | {% endfor %} 84 |
85 | {% endfor %} 86 | {% endfor %} 87 |
88 |
89 |
90 | {% endfor %} 91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/Renderer/TwigRenderer.php: -------------------------------------------------------------------------------- 1 | render('index.html.twig', 41 | array( 42 | 'suites' => $obj->getSuites(), 43 | 'failedScenarios' => $obj->getFailedScenarios(), 44 | 'pendingScenarios' => $obj->getPendingScenarios(), 45 | 'passedScenarios' => $obj->getPassedScenarios(), 46 | 'failedSteps' => $obj->getFailedSteps(), 47 | 'passedSteps' => $obj->getPassedSteps(), 48 | 'skippedSteps' => $obj->getSkippedSteps(), 49 | 'failedFeatures' => $obj->getFailedFeatures(), 50 | 'passedFeatures' => $obj->getPassedFeatures(), 51 | 'printStepArgs' => $obj->getPrintArguments(), 52 | 'printStepOuts' => $obj->getPrintOutputs(), 53 | 'printLoopBreak' => $obj->getPrintLoopBreak(), 54 | ) 55 | ); 56 | 57 | return $print; 58 | } 59 | 60 | /** 61 | * Renders before a suite. 62 | * 63 | * @param BehatHTMLFormatter $obj 64 | * 65 | * @return string : HTML generated 66 | */ 67 | public function renderBeforeSuite(BehatHTMLFormatter $obj) 68 | { 69 | return ''; 70 | } 71 | 72 | /** 73 | * Renders after a suite. 74 | * 75 | * @param BehatHTMLFormatter $obj 76 | * 77 | * @return string : HTML generated 78 | */ 79 | public function renderAfterSuite(BehatHTMLFormatter $obj) 80 | { 81 | return ''; 82 | } 83 | 84 | /** 85 | * Renders before a feature. 86 | * 87 | * @param BehatHTMLFormatter $obj 88 | * 89 | * @return string : HTML generated 90 | */ 91 | public function renderBeforeFeature(BehatHTMLFormatter $obj) 92 | { 93 | return ''; 94 | } 95 | 96 | /** 97 | * Renders after a feature. 98 | * 99 | * @param BehatHTMLFormatter $obj 100 | * 101 | * @return string : HTML generated 102 | */ 103 | public function renderAfterFeature(BehatHTMLFormatter $obj) 104 | { 105 | return ''; 106 | } 107 | 108 | /** 109 | * Renders before a scenario. 110 | * 111 | * @param BehatHTMLFormatter $obj 112 | * 113 | * @return string : HTML generated 114 | */ 115 | public function renderBeforeScenario(BehatHTMLFormatter $obj) 116 | { 117 | return ''; 118 | } 119 | 120 | /** 121 | * Renders after a scenario. 122 | * 123 | * @param BehatHTMLFormatter $obj 124 | * 125 | * @return string : HTML generated 126 | */ 127 | public function renderAfterScenario(BehatHTMLFormatter $obj) 128 | { 129 | return ''; 130 | } 131 | 132 | /** 133 | * Renders before an outline. 134 | * 135 | * @param BehatHTMLFormatter $obj 136 | * 137 | * @return string : HTML generated 138 | */ 139 | public function renderBeforeOutline(BehatHTMLFormatter $obj) 140 | { 141 | return ''; 142 | } 143 | 144 | /** 145 | * Renders after an outline. 146 | * 147 | * @param BehatHTMLFormatter $obj 148 | * 149 | * @return string : HTML generated 150 | */ 151 | public function renderAfterOutline(BehatHTMLFormatter $obj) 152 | { 153 | return ''; 154 | } 155 | 156 | /** 157 | * Renders before a step. 158 | * 159 | * @param BehatHTMLFormatter $obj 160 | * 161 | * @return string : HTML generated 162 | */ 163 | public function renderBeforeStep(BehatHTMLFormatter $obj) 164 | { 165 | return ''; 166 | } 167 | 168 | /** 169 | * Renders after a step. 170 | * 171 | * @param BehatHTMLFormatter $obj 172 | * 173 | * @return string : HTML generated 174 | */ 175 | public function renderAfterStep(BehatHTMLFormatter $obj) 176 | { 177 | return ''; 178 | } 179 | 180 | /** 181 | * To include CSS. 182 | * 183 | * @return string : HTML generated 184 | */ 185 | public function getCSS() 186 | { 187 | return ''; 188 | } 189 | 190 | /** 191 | * To include JS. 192 | * 193 | * @return string : HTML generated 194 | */ 195 | public function getJS() 196 | { 197 | return ''; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Renderer/MinimalRenderer.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace emuse\BehatHTMLFormatter\Renderer; 9 | 10 | class MinimalRenderer 11 | { 12 | private $extension = 'csv'; 13 | 14 | public function __construct() 15 | { 16 | } 17 | 18 | public function getExtension($renderer) 19 | { 20 | return $this->rendererList[$renderer]->getExtension(); 21 | } 22 | 23 | /** 24 | * Renders before an exercice. 25 | * 26 | * @param object : BehatHTMLFormatter object 27 | * 28 | * @return string : HTML generated 29 | */ 30 | public function renderBeforeExercise($obj) 31 | { 32 | return ''; 33 | } 34 | 35 | /** 36 | * Renders after an exercice. 37 | * 38 | * @param object : BehatHTMLFormatter object 39 | * 40 | * @return string : HTML generated 41 | */ 42 | public function renderAfterExercise($obj) 43 | { 44 | $strFeatPassed = count($obj->getPassedFeatures()); 45 | $strFeatFailed = count($obj->getFailedFeatures()); 46 | $strScePassed = count($obj->getPassedScenarios()); 47 | $strScePending = count($obj->getPendingScenarios()); 48 | $strSceFailed = count($obj->getFailedScenarios()); 49 | $strStepsPassed = count($obj->getPassedSteps()); 50 | $strStepsPending = count($obj->getPendingSteps()); 51 | $strStepsSkipped = count($obj->getSkippedSteps()); 52 | $strStepsFailed = count($obj->getFailedSteps()); 53 | 54 | $featTotal = (count($obj->getFailedFeatures()) + count($obj->getPassedFeatures())); 55 | $sceTotal = (count($obj->getFailedScenarios()) + count($obj->getPendingScenarios()) + count($obj->getPassedScenarios())); 56 | $stepsTotal = (count($obj->getFailedSteps()) + count($obj->getPassedSteps()) + count($obj->getSkippedSteps()) + count($obj->getPendingSteps())); 57 | 58 | $print = $featTotal.','.$strFeatPassed.','.$strFeatFailed."\n"; 59 | $print .= $sceTotal.','.$strScePassed.','.$strScePending.','.$strSceFailed."\n"; 60 | $print .= $stepsTotal.','.$strStepsPassed.','.$strStepsFailed.','.$strStepsSkipped.','.$strStepsPending."\n"; 61 | $print .= $obj->getTimer().','.$obj->getMemory()."\n"; 62 | 63 | return $print; 64 | } 65 | 66 | /** 67 | * Renders before a suite. 68 | * 69 | * @param object : BehatHTMLFormatter object 70 | * 71 | * @return string : HTML generated 72 | */ 73 | public function renderBeforeSuite($obj) 74 | { 75 | return ''; 76 | } 77 | 78 | /** 79 | * Renders after a suite. 80 | * 81 | * @param object : BehatHTMLFormatter object 82 | * 83 | * @return string : HTML generated 84 | */ 85 | public function renderAfterSuite($obj) 86 | { 87 | return ''; 88 | } 89 | 90 | /** 91 | * Renders before a feature. 92 | * 93 | * @param object : BehatHTMLFormatter object 94 | * 95 | * @return string : HTML generated 96 | */ 97 | public function renderBeforeFeature($obj) 98 | { 99 | return ''; 100 | } 101 | 102 | /** 103 | * Renders after a feature. 104 | * 105 | * @param object : BehatHTMLFormatter object 106 | * 107 | * @return string : HTML generated 108 | */ 109 | public function renderAfterFeature($obj) 110 | { 111 | return ''; 112 | } 113 | 114 | /** 115 | * Renders before a scenario. 116 | * 117 | * @param object : BehatHTMLFormatter object 118 | * 119 | * @return string : HTML generated 120 | */ 121 | public function renderBeforeScenario($obj) 122 | { 123 | return ''; 124 | } 125 | 126 | /** 127 | * Renders after a scenario. 128 | * 129 | * @param object : BehatHTMLFormatter object 130 | * 131 | * @return string : HTML generated 132 | */ 133 | public function renderAfterScenario($obj) 134 | { 135 | return ''; 136 | } 137 | 138 | /** 139 | * Renders before an outline. 140 | * 141 | * @param object : BehatHTMLFormatter object 142 | * 143 | * @return string : HTML generated 144 | */ 145 | public function renderBeforeOutline($obj) 146 | { 147 | return ''; 148 | } 149 | 150 | /** 151 | * Renders after an outline. 152 | * 153 | * @param object : BehatHTMLFormatter object 154 | * 155 | * @return string : HTML generated 156 | */ 157 | public function renderAfterOutline($obj) 158 | { 159 | return ''; 160 | } 161 | 162 | /** 163 | * Renders before a step. 164 | * 165 | * @param object : BehatHTMLFormatter object 166 | * 167 | * @return string : HTML generated 168 | */ 169 | public function renderBeforeStep($obj) 170 | { 171 | return ''; 172 | } 173 | 174 | /** 175 | * Renders after a step. 176 | * 177 | * @param object : BehatHTMLFormatter object 178 | * 179 | * @return string : HTML generated 180 | */ 181 | public function renderAfterStep($obj) 182 | { 183 | return ''; 184 | } 185 | 186 | /** 187 | * To include CSS. 188 | * 189 | * @return string : HTML generated 190 | */ 191 | public function getCSS() 192 | { 193 | return ''; 194 | } 195 | 196 | /** 197 | * To include JS. 198 | * 199 | * @return string : HTML generated 200 | */ 201 | public function getJS() 202 | { 203 | return ''; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/Classes/Feature.php: -------------------------------------------------------------------------------- 1 | 8 | private $id; 9 | private $name; 10 | private $description; 11 | private $tags; 12 | private $file; 13 | private $screenshotFolder; 14 | private $failedScenarios = 0; 15 | private $pendingScenarios = 0; 16 | private $passedScenarios = 0; 17 | private $scenarioCounter = 1; 18 | 19 | /** 20 | * @var Scenario[] 21 | */ 22 | private $scenarios; 23 | // 24 | 25 | // 26 | 27 | /** 28 | * @return mixed 29 | */ 30 | public function getName() 31 | { 32 | return $this->name; 33 | } 34 | 35 | /** 36 | * @param mixed $name 37 | */ 38 | public function setName($name) 39 | { 40 | $this->name = $name; 41 | } 42 | 43 | /** 44 | * @return mixed 45 | */ 46 | public function getDescription() 47 | { 48 | return $this->description; 49 | } 50 | 51 | /** 52 | * @param mixed $description 53 | */ 54 | public function setDescription($description) 55 | { 56 | $this->description = $description; 57 | } 58 | 59 | /** 60 | * @return mixed 61 | */ 62 | public function getTags() 63 | { 64 | return $this->tags; 65 | } 66 | 67 | /** 68 | * @param mixed $tags 69 | */ 70 | public function setTags($tags) 71 | { 72 | $this->tags = $tags; 73 | } 74 | 75 | /** 76 | * @return mixed 77 | */ 78 | public function getFile() 79 | { 80 | return $this->file; 81 | } 82 | 83 | /** 84 | * @param mixed $file 85 | */ 86 | public function setFile($file) 87 | { 88 | $this->file = $file; 89 | } 90 | 91 | /** 92 | * @return mixed 93 | */ 94 | public function getScreenshotFolder() 95 | { 96 | return $this->screenshotFolder; 97 | } 98 | 99 | /** 100 | * @param string $featureName 101 | */ 102 | public function setScreenshotFolder($featureName) 103 | { 104 | $this->screenshotFolder = preg_replace('/\W/', '', $featureName); 105 | } 106 | 107 | /** 108 | * @return Scenario[] 109 | */ 110 | public function getScenarios() 111 | { 112 | return $this->scenarios; 113 | } 114 | 115 | /** 116 | * @param Scenario[] $scenarios 117 | */ 118 | public function setScenarios($scenarios) 119 | { 120 | $this->scenarios = $scenarios; 121 | } 122 | 123 | /** 124 | * @param $scenario Scenario 125 | */ 126 | public function addScenario($scenario) 127 | { 128 | $scenario->setId($this->scenarioCounter); 129 | ++$this->scenarioCounter; 130 | $this->scenarios[] = $scenario; 131 | } 132 | 133 | /** 134 | * @return mixed 135 | */ 136 | public function getFailedScenarios() 137 | { 138 | return $this->failedScenarios; 139 | } 140 | 141 | /** 142 | * @param mixed $failedScenarios 143 | */ 144 | public function setFailedScenarios($failedScenarios) 145 | { 146 | $this->failedScenarios = $failedScenarios; 147 | } 148 | 149 | public function addFailedScenario($number = 1) 150 | { 151 | $this->failedScenarios += $number; 152 | } 153 | 154 | /** 155 | * @return mixed 156 | */ 157 | public function getPendingScenarios() 158 | { 159 | return $this->pendingScenarios; 160 | } 161 | 162 | /** 163 | * @param mixed $pendingScenarios 164 | */ 165 | public function setPendingScenarios($pendingScenarios) 166 | { 167 | $this->pendingScenarios = $pendingScenarios; 168 | } 169 | 170 | public function addPendingScenario($number = 1) 171 | { 172 | $this->pendingScenarios += $number; 173 | } 174 | 175 | /** 176 | * @return mixed 177 | */ 178 | public function getPassedScenarios() 179 | { 180 | return $this->passedScenarios; 181 | } 182 | 183 | /** 184 | * @param mixed $passedScenarios 185 | */ 186 | public function setPassedScenarios($passedScenarios) 187 | { 188 | $this->passedScenarios = $passedScenarios; 189 | } 190 | 191 | public function addPassedScenario($number = 1) 192 | { 193 | $this->passedScenarios += $number; 194 | } 195 | 196 | /** 197 | * @return mixed 198 | */ 199 | public function getId() 200 | { 201 | return $this->id; 202 | } 203 | 204 | /** 205 | * @param mixed $id 206 | */ 207 | public function setId($id) 208 | { 209 | $this->id = $id; 210 | } 211 | 212 | // 213 | 214 | // 215 | public function allPassed() 216 | { 217 | if (0 == $this->failedScenarios) { 218 | return true; 219 | } 220 | 221 | return false; 222 | } 223 | 224 | public function getPassedClass() 225 | { 226 | if ($this->allPassed()) { 227 | return 'passed'; 228 | } 229 | 230 | return 'failed'; 231 | } 232 | 233 | public function getPercentPassed() 234 | { 235 | return ($this->getPassedScenarios() / ($this->getTotalAmountOfScenarios())) * 100; 236 | } 237 | 238 | public function getPercentPending() 239 | { 240 | return ($this->getPendingScenarios() / ($this->getTotalAmountOfScenarios())) * 100; 241 | } 242 | 243 | public function getPercentFailed() 244 | { 245 | return ($this->getFailedScenarios() / ($this->getTotalAmountOfScenarios())) * 100; 246 | } 247 | 248 | public function getTotalAmountOfScenarios() 249 | { 250 | return $this->getPassedScenarios() + $this->getPendingScenarios() + $this->getFailedScenarios(); 251 | } 252 | 253 | // 254 | } 255 | -------------------------------------------------------------------------------- /src/Renderer/BaseRenderer.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace emuse\BehatHTMLFormatter\Renderer; 9 | 10 | class BaseRenderer 11 | { 12 | /** 13 | * @var : List of the renderer names 14 | */ 15 | private $nameList; 16 | 17 | /** 18 | * @var : List of the renderer objects 19 | */ 20 | private $rendererList; 21 | 22 | /** 23 | * Constructor : load the renderers. 24 | * 25 | * @param string : list of the renderer 26 | * @param string : base_path 27 | */ 28 | public function __construct($renderer, $base_path) 29 | { 30 | $rendererList = explode(',', $renderer); 31 | 32 | $this->nameList = array(); 33 | $this->rendererList = array(); 34 | 35 | //let's load the renderer dynamically 36 | foreach ($rendererList as $renderer) { 37 | $this->nameList[] = $renderer; 38 | if (in_array($renderer, array('Behat2', 'Twig', 'Minimal'))) { 39 | $className = __NAMESPACE__.'\\'.$renderer.'Renderer'; 40 | } else { 41 | $className = $renderer; 42 | } 43 | $this->rendererList[$renderer] = new $className(); 44 | } 45 | } 46 | 47 | /** 48 | * Return the list of the name of the renderers. 49 | * 50 | * @return array 51 | */ 52 | public function getNameList() 53 | { 54 | return $this->nameList; 55 | } 56 | 57 | /** 58 | * Renders before an exercice. 59 | * 60 | * @param object : BehatHTMLFormatter object 61 | * 62 | * @return string : HTML generated 63 | */ 64 | public function renderBeforeExercise($obj) 65 | { 66 | $print = array(); 67 | foreach ($this->rendererList as $name => $renderer) { 68 | $print[$name] = $renderer->renderBeforeExercise($obj); 69 | } 70 | 71 | return $print; 72 | } 73 | 74 | /** 75 | * Renders after an exercice. 76 | * 77 | * @param object : BehatHTMLFormatter object 78 | * 79 | * @return string : HTML generated 80 | */ 81 | public function renderAfterExercise($obj) 82 | { 83 | $print = array(); 84 | foreach ($this->rendererList as $name => $renderer) { 85 | $print[$name] = $renderer->renderAfterExercise($obj); 86 | } 87 | 88 | return $print; 89 | } 90 | 91 | /** 92 | * Renders before a suite. 93 | * 94 | * @param object : BehatHTMLFormatter object 95 | * 96 | * @return string : HTML generated 97 | */ 98 | public function renderBeforeSuite($obj) 99 | { 100 | $print = array(); 101 | foreach ($this->rendererList as $name => $renderer) { 102 | $print[$name] = $renderer->renderBeforeSuite($obj); 103 | } 104 | 105 | return $print; 106 | } 107 | 108 | /** 109 | * Renders after a suite. 110 | * 111 | * @param object : BehatHTMLFormatter object 112 | * 113 | * @return string : HTML generated 114 | */ 115 | public function renderAfterSuite($obj) 116 | { 117 | $print = array(); 118 | foreach ($this->rendererList as $name => $renderer) { 119 | $print[$name] = $renderer->renderAfterSuite($obj); 120 | } 121 | 122 | return $print; 123 | } 124 | 125 | /** 126 | * Renders before a feature. 127 | * 128 | * @param object : BehatHTMLFormatter object 129 | * 130 | * @return string : HTML generated 131 | */ 132 | public function renderBeforeFeature($obj) 133 | { 134 | $print = array(); 135 | foreach ($this->rendererList as $name => $renderer) { 136 | $print[$name] = $renderer->renderBeforeFeature($obj); 137 | } 138 | 139 | return $print; 140 | } 141 | 142 | /** 143 | * Renders after a feature. 144 | * 145 | * @param object : BehatHTMLFormatter object 146 | * 147 | * @return string : HTML generated 148 | */ 149 | public function renderAfterFeature($obj) 150 | { 151 | $print = array(); 152 | foreach ($this->rendererList as $name => $renderer) { 153 | $print[$name] = $renderer->renderAfterFeature($obj); 154 | } 155 | 156 | return $print; 157 | } 158 | 159 | /** 160 | * Renders before a scenario. 161 | * 162 | * @param object : BehatHTMLFormatter object 163 | * 164 | * @return string : HTML generated 165 | */ 166 | public function renderBeforeScenario($obj) 167 | { 168 | $print = array(); 169 | foreach ($this->rendererList as $name => $renderer) { 170 | $print[$name] = $renderer->renderBeforeScenario($obj); 171 | } 172 | 173 | return $print; 174 | } 175 | 176 | /** 177 | * Renders after a scenario. 178 | * 179 | * @param object : BehatHTMLFormatter object 180 | * 181 | * @return string : HTML generated 182 | */ 183 | public function renderAfterScenario($obj) 184 | { 185 | $print = array(); 186 | foreach ($this->rendererList as $name => $renderer) { 187 | $print[$name] = $renderer->renderAfterScenario($obj); 188 | } 189 | 190 | return $print; 191 | } 192 | 193 | /** 194 | * Renders before an outline. 195 | * 196 | * @param object : BehatHTMLFormatter object 197 | * 198 | * @return string : HTML generated 199 | */ 200 | public function renderBeforeOutline($obj) 201 | { 202 | $print = array(); 203 | foreach ($this->rendererList as $name => $renderer) { 204 | $print[$name] = $renderer->renderBeforeOutline($obj); 205 | } 206 | 207 | return $print; 208 | } 209 | 210 | /** 211 | * Renders after an outline. 212 | * 213 | * @param object : BehatHTMLFormatter object 214 | * 215 | * @return string : HTML generated 216 | */ 217 | public function renderAfterOutline($obj) 218 | { 219 | $print = array(); 220 | foreach ($this->rendererList as $name => $renderer) { 221 | $print[$name] = $renderer->renderAfterOutline($obj); 222 | } 223 | 224 | return $print; 225 | } 226 | 227 | /** 228 | * Renders before a step. 229 | * 230 | * @param object : BehatHTMLFormatter object 231 | * 232 | * @return string : HTML generated 233 | */ 234 | public function renderBeforeStep($obj) 235 | { 236 | $print = array(); 237 | foreach ($this->rendererList as $name => $renderer) { 238 | $print[$name] = $renderer->renderBeforeStep($obj); 239 | } 240 | 241 | return $print; 242 | } 243 | 244 | /** 245 | * Renders after a step. 246 | * 247 | * @param object : BehatHTMLFormatter object 248 | * 249 | * @return string : HTML generated 250 | */ 251 | public function renderAfterStep($obj) 252 | { 253 | $print = array(); 254 | foreach ($this->rendererList as $name => $renderer) { 255 | $print[$name] = $renderer->renderAfterStep($obj); 256 | } 257 | 258 | return $print; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## BehatHtmlFormatterPlugin 2 | 3 | Behat 3 extension for generating HTML reports from your test results. 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/emuse/behat-html-formatter/v/stable)](https://packagist.org/packages/emuse/behat-html-formatter) [![Total Downloads](https://poser.pugx.org/emuse/behat-html-formatter/downloads)](https://packagist.org/packages/emuse/behat-html-formatter) [![Latest Unstable Version](https://poser.pugx.org/emuse/behat-html-formatter/v/unstable)](https://packagist.org/packages/emuse/behat-html-formatter) [![License](https://poser.pugx.org/emuse/behat-html-formatter/license)](https://packagist.org/packages/emuse/behat-html-formatter) 6 | 7 | ### Twig report 8 | 9 | ![Twig Screenshot](http://i.imgur.com/o0zCqiB.png) 10 | 11 | ### Behat 2 report 12 | 13 | ![Behat2 Screenshot](http://i57.tinypic.com/287g942.jpg) 14 | 15 | 16 | ## How? 17 | 18 | * The tool can be installed easily with composer. 19 | * Defining the formatter in the `behat.yml` file 20 | * Modifying the settings in the `behat.yml`file 21 | 22 | ## Installation 23 | 24 | ### Prerequisites 25 | 26 | This extension requires: 27 | 28 | * PHP 5.3.x or higher 29 | * Behat 3.x or higher 30 | 31 | ### Through composer 32 | 33 | The easiest way to keep your suite updated is to use [Composer](http://getcomposer.org>): 34 | 35 | #### Install with composer: 36 | 37 | ```bash 38 | $ composer require --dev emuse/behat-html-formatter 39 | ``` 40 | 41 | #### Install using `composer.json` 42 | 43 | Add BehatHtmlFormatterPlugin to the list of dependencies inside your `composer.json`. 44 | 45 | ```json 46 | { 47 | "require": { 48 | "behat/behat": "3.*@stable", 49 | "emuse/behat-html-formatter": "0.1.*", 50 | }, 51 | "minimum-stability": "dev", 52 | "config": { 53 | "bin-dir": "bin/" 54 | } 55 | } 56 | ``` 57 | 58 | Then simply install it with composer: 59 | 60 | ```bash 61 | $ composer install --dev --prefer-dist 62 | ``` 63 | 64 | You can read more about Composer on its [official webpage](http://getcomposer.org). 65 | 66 | ## Basic usage 67 | 68 | Activate the extension by specifying its class in your `behat.yml`: 69 | 70 | ```json 71 | # behat.yml 72 | default: 73 | suites: 74 | default: 75 | contexts: 76 | - emuse\BehatHTMLFormatter\Context\ScreenshotContext: 77 | screenshotDir: build/html/behat/assets/screenshots 78 | ... # All your awesome suites come here 79 | formatters: 80 | html: 81 | output_path: %paths.base%/build/html/behat 82 | 83 | extensions: 84 | emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension: 85 | name: html 86 | renderer: Twig,Behat2 87 | file_name: index 88 | print_args: true 89 | print_outp: true 90 | loop_break: true 91 | ``` 92 | 93 | ### Command line options 94 | 95 | Add the following to your behat command to print a report: 96 | 97 | `behat --format html --out MYDIRECTORY` 98 | 99 | Setting the format to html will output the various reports that you configure below (Behat2, Twig, Minimal, etc.) 100 | 101 | You also need to specify the output directory for the reports as MYDIRECTORY. 102 | 103 | ## Configuration 104 | 105 | ### Formatter configuration 106 | 107 | * `output_path` - The location where Behat will save the HTML reports. Use `%paths.base%` to build the full path. 108 | 109 | ### Extension configuration 110 | 111 | * `renderer` - The engine that Behat will use for rendering, thus the types of report format Behat should output (multiple report formats are allowed, separate them by commas). Allowed values are: 112 | * *Behat2* for generating HTML reports like they were generated in Behat 2. 113 | * *Twig* A new and more modern format based on Twig. 114 | * *Minimal* An ultra minimal HTML output. 115 | * `file_name` - (Optional) Behat will use a fixed filename and overwrite the same file after each build. By default, Behat will create a new HTML file using a random name (*"renderer name"*_*"date hour"*). 116 | * `print_args` - (Optional) If set to `true`, Behat will add all arguments for each step to the report. (E.g. Tables). 117 | * `print_outp` - (Optional) If set to `true`, Behat will add the output of each step to the report. (E.g. Exceptions). 118 | * `loop_break` - (Optional) If set to `true`, Behat will add a separating break line after each execution when printing Scenario Outlines. 119 | 120 | ## Screenshot 121 | 122 | The facility exists to embed a screenshot into test failures. 123 | 124 | Currently png is the only supported image format. 125 | 126 | In order to embed a screenshot, you will need to take a screenshot using your favourite webdriver and store it in the following filepath format: 127 | 128 | results/html/assets/screenshots/{{feature_name}}/{{scenario_name}}.png 129 | 130 | The feature_name and scenario_name variables will need to be the relevant item names without spaces. 131 | 132 | Below is an example of FeatureContext methods which will produce an image file in the above format: 133 | 134 | ```php 135 | 136 | /** 137 | * @BeforeScenario 138 | * 139 | * @param BeforeScenarioScope $scope 140 | * 141 | */ 142 | public function setUpTestEnvironment($scope) 143 | { 144 | $this->currentScenario = $scope->getScenario(); 145 | } 146 | 147 | /** 148 | * @AfterStep 149 | * 150 | * @param AfterStepScope $scope 151 | */ 152 | public function afterStep($scope) 153 | { 154 | //if test has failed, and is not an api test, get screenshot 155 | if(!$scope->getTestResult()->isPassed()) 156 | { 157 | //create filename string 158 | 159 | $featureFolder = preg_replace('/\W/', '', $scope->getFeature()->getTitle()); 160 | 161 | $scenarioName = $this->currentScenario->getTitle(); 162 | $fileName = preg_replace('/\W/', '', $scenarioName) . '.png'; 163 | 164 | //create screenshots directory if it doesn't exist 165 | if (!file_exists('results/html/assets/screenshots/' . $featureFolder)) { 166 | mkdir('results/html/assets/screenshots/' . $featureFolder); 167 | } 168 | 169 | //take screenshot and save as the previously defined filename 170 | $this->driver->takeScreenshot('results/html/assets/screenshots/' . $featureFolder . '/' . $fileName); 171 | // For Selenium2 Driver you can use: 172 | // file_put_contents('results/html/assets/screenshots/' . $featureFolder . '/' . $fileName, $this->getSession()->getDriver()->getScreenshot()); 173 | } 174 | } 175 | 176 | ``` 177 | 178 | Note that the currentScenario variable will need to be at class level and generated in the @BeforeScenario method as Behat does not currently support obtaining the current Scenario in the @AfterStep method, where the screenshot is generated 179 | 180 | ## Issue Submission 181 | 182 | When you need additional support or you discover something *strange*, feel free to [Create a new issue](https://github.com/dutchiexl/BehatHtmlFormatterPlugin/issues/new). 183 | 184 | ## License and Authors 185 | 186 | Authors: https://github.com/dutchiexl/BehatHtmlFormatterPlugin/contributors 187 | 188 | 189 | -------------------------------------------------------------------------------- /src/Printer/FileOutputPrinter.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace emuse\BehatHTMLFormatter\Printer; 9 | 10 | use Behat\Testwork\Output\Exception\BadOutputPathException; 11 | use Behat\Testwork\Output\Printer\OutputPrinter as PrinterInterface; 12 | 13 | class FileOutputPrinter implements PrinterInterface 14 | { 15 | /** 16 | * @param $outputPath where to save the generated report file 17 | */ 18 | private $outputPath; 19 | 20 | /** 21 | * @param $base_path Behat base path 22 | */ 23 | private $base_path; 24 | 25 | /** 26 | * @param $rendererFiles List of the filenames for the renderers 27 | */ 28 | private $rendererFiles; 29 | 30 | /** 31 | * @param $rendererList 32 | * @param $filename 33 | * @param $base_path 34 | */ 35 | public function __construct($rendererList, $filename, $base_path) 36 | { 37 | //let's generate the filenames for the renderers 38 | $this->rendererFiles = array(); 39 | foreach ($rendererList as $renderer) { 40 | if ('generated' == $filename) { 41 | $date = date('YmdHis'); 42 | $this->rendererFiles[$renderer] = $renderer.'_'.$date; 43 | } else { 44 | $this->rendererFiles[$renderer] = $filename; 45 | } 46 | } 47 | 48 | $this->base_path = $base_path; 49 | } 50 | 51 | /** 52 | * Verify that the specified output path exists or can be created, 53 | * then sets the output path. 54 | * 55 | * @param string $path Output path relative to %paths.base% 56 | */ 57 | public function setOutputPath($path) 58 | { 59 | $outpath = $path; 60 | if (!file_exists($outpath)) { 61 | if (!mkdir($outpath, 0755, true)) { 62 | throw new BadOutputPathException( 63 | sprintf( 64 | 'Output path %s does not exist and could not be created!', 65 | $outpath 66 | ), 67 | $outpath 68 | ); 69 | } 70 | } else { 71 | if (!is_dir(realpath($outpath))) { 72 | throw new BadOutputPathException( 73 | sprintf( 74 | 'The argument to `output` is expected to the a directory, but got %s!', 75 | $outpath 76 | ), 77 | $outpath 78 | ); 79 | } 80 | } 81 | $this->outputPath = $outpath; 82 | } 83 | 84 | /** 85 | * Returns output path. 86 | * 87 | * @return string output path 88 | */ 89 | public function getOutputPath() 90 | { 91 | return $this->outputPath; 92 | } 93 | 94 | /** 95 | * Sets output styles. 96 | * 97 | * @param array $styles 98 | */ 99 | public function setOutputStyles(array $styles) 100 | { 101 | } 102 | 103 | /** 104 | * Returns output styles. 105 | * 106 | * @return array 107 | */ 108 | public function getOutputStyles() 109 | { 110 | } 111 | 112 | /** 113 | * Forces output to be decorated. 114 | * 115 | * @param bool $decorated 116 | */ 117 | public function setOutputDecorated($decorated) 118 | { 119 | } 120 | 121 | /** 122 | * Returns output decoration status. 123 | * 124 | * @return null|bool 125 | */ 126 | public function isOutputDecorated() 127 | { 128 | return true; 129 | } 130 | 131 | /** 132 | * Sets output verbosity level. 133 | * 134 | * @param int $level 135 | */ 136 | public function setOutputVerbosity($level) 137 | { 138 | } 139 | 140 | /** 141 | * Returns output verbosity level. 142 | * 143 | * @return int 144 | */ 145 | public function getOutputVerbosity() 146 | { 147 | } 148 | 149 | /** 150 | * Writes message(s) to output console. 151 | * 152 | * @param string|array $messages message or array of messages 153 | */ 154 | public function write($messages = array()) 155 | { 156 | //Write it for each message = each renderer 157 | foreach ($messages as $key => $message) { 158 | $file = $this->outputPath.DIRECTORY_SEPARATOR.$this->rendererFiles[$key].'.html'; 159 | file_put_contents($file, $message); 160 | $this->copyAssets($key); 161 | } 162 | } 163 | 164 | /** 165 | * Writes newlined message(s) to output console. 166 | * 167 | * @param string|array $messages message or array of messages 168 | */ 169 | public function writeln($messages = array()) 170 | { 171 | //Write it for each message = each renderer 172 | foreach ($messages as $key => $message) { 173 | $file = $this->outputPath.DIRECTORY_SEPARATOR.$this->rendererFiles[$key].'.html'; 174 | file_put_contents($file, $message, FILE_APPEND); 175 | } 176 | } 177 | 178 | /** 179 | * Writes message(s) at start of the output console. 180 | * 181 | * @param string|array $messages message or array of messages 182 | */ 183 | public function writeBeginning($messages = array()) 184 | { 185 | //Write it for each message = each renderer 186 | foreach ($messages as $key => $message) { 187 | $file = $this->outputPath.DIRECTORY_SEPARATOR.$this->rendererFiles[$key].'.html'; 188 | $fileContents = file_get_contents($file); 189 | file_put_contents($file, $message.$fileContents); 190 | } 191 | } 192 | 193 | /** 194 | * Copies the assets folder to the report destination. 195 | * 196 | * @param string : the renderer 197 | */ 198 | public function copyAssets($renderer) 199 | { 200 | // If the assets folder doesn' exist in the output path for this renderer, copy it 201 | $source = realpath(dirname(__FILE__)); 202 | $assets_source = realpath($source.'/../../assets/'.$renderer); 203 | if (false === $assets_source) { 204 | //There is no assets to copy for this renderer 205 | return; 206 | } 207 | 208 | //first create the assets dir 209 | $destination = $this->outputPath.DIRECTORY_SEPARATOR.'assets'; 210 | @mkdir($destination); 211 | 212 | $this->recurse_copy($assets_source, $destination.DIRECTORY_SEPARATOR.$renderer); 213 | } 214 | 215 | /** 216 | * Recursivly copy a path. 217 | * 218 | * @param $src 219 | * @param $dst 220 | */ 221 | private function recurse_copy($src, $dst) 222 | { 223 | $dir = opendir($src); 224 | @mkdir($dst); 225 | while (false !== ($file = readdir($dir))) { 226 | if (('.' != $file) && ('..' != $file)) { 227 | if (is_dir($src.'/'.$file)) { 228 | $this->recurse_copy($src.'/'.$file, $dst.'/'.$file); 229 | } else { 230 | copy($src.'/'.$file, $dst.'/'.$file); 231 | } 232 | } 233 | } 234 | closedir($dir); 235 | } 236 | 237 | /** 238 | * Clear output console, so on next write formatter will need to init (create) it again. 239 | */ 240 | public function flush() 241 | { 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | use Behat\Behat\Tester\Exception\PendingException; 12 | use Behat\Behat\Context\Context; 13 | use Behat\Gherkin\Node\PyStringNode; 14 | use Symfony\Component\Process\PhpExecutableFinder; 15 | use Symfony\Component\Process\Process; 16 | use Behat\Behat\Context\SnippetAcceptingContext; 17 | 18 | /** 19 | * Behat test suite context. 20 | * 21 | * @author Konstantin Kudryashov 22 | */ 23 | class FeatureContext implements Context, SnippetAcceptingContext 24 | { 25 | /** 26 | * @var string 27 | */ 28 | private $phpBin; 29 | /** 30 | * @var Process 31 | */ 32 | private $process; 33 | /** 34 | * @var string 35 | */ 36 | private $workingDir; 37 | /** 38 | * @var string 39 | */ 40 | private $output; 41 | /** 42 | * @var string 43 | */ 44 | private $reportDir; 45 | 46 | /** 47 | * Cleans test folders in the temporary directory. 48 | * 49 | * @BeforeSuite 50 | * @After Suite 51 | */ 52 | public static function cleanTestFolders() 53 | { 54 | if (is_dir($dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'behat')) { 55 | self::clearDirectory($dir); 56 | } 57 | } 58 | 59 | /** 60 | * @AfterScenario 61 | */ 62 | public function copyResultingTemplateToProjectRoot() 63 | { 64 | shell_exec('rm -rf '.__DIR__.'/../../build'); 65 | mkdir(__DIR__.'/../../build'); 66 | shell_exec('cp -R '.$this->workingDir.'/build/* '.__DIR__.'/../../build'); 67 | } 68 | 69 | /** 70 | * Prepares test folders in the temporary directory. 71 | * 72 | * @BeforeScenario 73 | */ 74 | public function prepareTestFolders() 75 | { 76 | $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'behat' . DIRECTORY_SEPARATOR . 77 | md5(microtime() * rand(0, 10000)); 78 | 79 | mkdir($dir . '/features/bootstrap/i18n', 0777, true); 80 | mkdir($dir . '/junit'); 81 | 82 | $phpFinder = new PhpExecutableFinder(); 83 | if (false === $php = $phpFinder->find()) { 84 | throw new \RuntimeException('Unable to find the PHP executable.'); 85 | } 86 | $this->workingDir = $dir; 87 | $this->phpBin = $php; 88 | $this->process = new Process(null); 89 | $this->reportDir = $this->workingDir . '/build/'; 90 | } 91 | 92 | /** 93 | * Creates a file with specified name and context in current workdir. 94 | * 95 | * @Given /^(?:there is )?a file named "([^"]*)" with:$/ 96 | * 97 | * @param string $filename name of the file (relative path) 98 | * @param PyStringNode $content PyString string instance 99 | */ 100 | public function aFileNamedWith($filename, PyStringNode $content) 101 | { 102 | $content = strtr((string) $content, array("'''" => '"""')); 103 | $this->createFile($this->workingDir . '/' . $filename, $content); 104 | } 105 | 106 | /** 107 | * Moves user to the specified path. 108 | * 109 | * @Given /^I am in the "([^"]*)" path$/ 110 | * 111 | * @param string $path 112 | */ 113 | public function iAmInThePath($path) 114 | { 115 | $this->moveToNewPath($path); 116 | } 117 | 118 | /** 119 | * Checks whether a file at provided path exists. 120 | * 121 | * @Given /^file "([^"]*)" should exist$/ 122 | * 123 | * @param string $path 124 | */ 125 | public function fileShouldExist($path) 126 | { 127 | PHPUnit_Framework_Assert::assertFileExists($this->workingDir . DIRECTORY_SEPARATOR . $path); 128 | } 129 | 130 | /** 131 | * Sets specified ENV variable 132 | * 133 | * @When /^"BEHAT_PARAMS" environment variable is set to:$/ 134 | * 135 | * @param PyStringNode $value 136 | */ 137 | public function iSetEnvironmentVariable(PyStringNode $value) 138 | { 139 | $this->process->setEnv(array('BEHAT_PARAMS' => (string) $value)); 140 | } 141 | 142 | /** 143 | * Runs behat command with provided parameters 144 | * 145 | * @When /^I run "behat(?: ((?:\"|[^"])*))?"$/ 146 | * 147 | * @param string $argumentsString 148 | */ 149 | public function iRunBehat($argumentsString = '') 150 | { 151 | $argumentsString = strtr($argumentsString, array('\'' => '"')); 152 | 153 | $this->process->setWorkingDirectory($this->workingDir); 154 | $this->process->setCommandLine( 155 | sprintf( 156 | '%s %s %s %s', 157 | $this->phpBin, 158 | escapeshellarg(BEHAT_BIN_PATH), 159 | $argumentsString, 160 | strtr('--format-settings=\'{"timer": false}\'', array('\'' => '"', '"' => '\"')) 161 | ) 162 | ); 163 | 164 | // Don't reset the LANG variable on HHVM, because it breaks HHVM itself 165 | if (!defined('HHVM_VERSION')) { 166 | $env = $this->process->getEnv(); 167 | $env['LANG'] = 'en'; // Ensures that the default language is en, whatever the OS locale is. 168 | $this->process->setEnv($env); 169 | } 170 | 171 | $this->process->run(); 172 | 173 | $this->output = $this->process->getOutput(); 174 | } 175 | 176 | /** 177 | * Checks whether previously ran command passes|fails with provided output. 178 | * 179 | * @Then /^it should (fail|pass) with:$/ 180 | * 181 | * @param string $success "fail" or "pass" 182 | * @param PyStringNode $text PyString text instance 183 | */ 184 | public function itShouldPassWith($success, PyStringNode $text) 185 | { 186 | $this->itShouldFail($success); 187 | $this->theOutputShouldContain($text); 188 | } 189 | 190 | /** 191 | * Checks whether previously runned command passes|failes with no output. 192 | * 193 | * @Then /^it should (fail|pass) with no output$/ 194 | * 195 | * @param string $success "fail" or "pass" 196 | */ 197 | public function itShouldPassWithNoOutput($success) 198 | { 199 | $this->itShouldFail($success); 200 | PHPUnit_Framework_Assert::assertEmpty($this->getOutput()); 201 | } 202 | 203 | /** 204 | * Checks whether specified file exists and contains specified string. 205 | * 206 | * @Then /^"([^"]*)" file should contain:$/ 207 | * 208 | * @param string $path file path 209 | * @param PyStringNode $text file content 210 | */ 211 | public function fileShouldContain($path, PyStringNode $text) 212 | { 213 | $path = $this->workingDir . '/' . $path; 214 | PHPUnit_Framework_Assert::assertFileExists($path); 215 | 216 | $fileContent = trim(file_get_contents($path)); 217 | // Normalize the line endings in the output 218 | if ("\n" !== PHP_EOL) { 219 | $fileContent = str_replace(PHP_EOL, "\n", $fileContent); 220 | } 221 | 222 | PHPUnit_Framework_Assert::assertEquals($this->getExpectedOutput($text), $fileContent); 223 | } 224 | 225 | /** 226 | * Checks whether specified content and structure of the xml is correct without worrying about layout. 227 | * 228 | * @Then /^"([^"]*)" file xml should be like:$/ 229 | * 230 | * @param string $path file path 231 | * @param PyStringNode $text file content 232 | */ 233 | public function fileXmlShouldBeLike($path, PyStringNode $text) 234 | { 235 | $path = $this->workingDir . '/' . $path; 236 | PHPUnit_Framework_Assert::assertFileExists($path); 237 | 238 | $fileContent = trim(file_get_contents($path)); 239 | 240 | $dom = new DOMDocument(); 241 | $dom->loadXML($text); 242 | $dom->formatOutput = true; 243 | 244 | PHPUnit_Framework_Assert::assertEquals(trim($dom->saveXML(null, LIBXML_NOEMPTYTAG)), $fileContent); 245 | } 246 | 247 | 248 | /** 249 | * Checks whether last command output contains provided string. 250 | * 251 | * @Then the output should contain: 252 | * 253 | * @param PyStringNode $text PyString text instance 254 | */ 255 | public function theOutputShouldContain(PyStringNode $text) 256 | { 257 | PHPUnit_Framework_Assert::assertContains($this->getExpectedOutput($text), $this->getOutput()); 258 | } 259 | 260 | private function getExpectedOutput(PyStringNode $expectedText) 261 | { 262 | $text = strtr($expectedText, array('\'\'\'' => '"""', '%%TMP_DIR%%' => sys_get_temp_dir() . DIRECTORY_SEPARATOR)); 263 | 264 | // windows path fix 265 | if ('/' !== DIRECTORY_SEPARATOR) { 266 | $text = preg_replace_callback( 267 | '/[ "]features\/[^\n "]+/', function ($matches) { 268 | return str_replace('/', DIRECTORY_SEPARATOR, $matches[0]); 269 | }, $text 270 | ); 271 | $text = preg_replace_callback( 272 | '/\features\/[^\<]+/', function ($matches) { 273 | return str_replace('/', DIRECTORY_SEPARATOR, $matches[0]); 274 | }, $text 275 | ); 276 | $text = preg_replace_callback( 277 | '/\+[fd] [^ ]+/', function ($matches) { 278 | return str_replace('/', DIRECTORY_SEPARATOR, $matches[0]); 279 | }, $text 280 | ); 281 | } 282 | 283 | return $text; 284 | } 285 | 286 | /** 287 | * Checks whether previously ran command failed|passed. 288 | * 289 | * @Then /^it should (fail|pass)$/ 290 | * 291 | * @param string $success "fail" or "pass" 292 | */ 293 | public function itShouldFail($success) 294 | { 295 | if ('fail' === $success) { 296 | if (0 === $this->getExitCode()) { 297 | echo 'Actual output:' . PHP_EOL . PHP_EOL . $this->getOutput(); 298 | } 299 | 300 | PHPUnit_Framework_Assert::assertNotEquals(0, $this->getExitCode()); 301 | } else { 302 | if (0 !== $this->getExitCode()) { 303 | echo 'Actual output:' . PHP_EOL . PHP_EOL . $this->getOutput(); 304 | } 305 | 306 | PHPUnit_Framework_Assert::assertEquals(0, $this->getExitCode()); 307 | } 308 | } 309 | 310 | /** 311 | * Checks whether the file is valid according to an XML schema. 312 | * 313 | * @Then /^the file "([^"]+)" should be a valid document according to "([^"]+)"$/ 314 | * 315 | * @param string $xmlFile 316 | * @param string $schemaPath relative to features/bootstrap/schema 317 | */ 318 | public function xmlShouldBeValid($xmlFile, $schemaPath) 319 | { 320 | $dom = new DomDocument(); 321 | $dom->load($this->workingDir . '/' . $xmlFile); 322 | 323 | $dom->schemaValidate(__DIR__ . '/schema/' . $schemaPath); 324 | } 325 | 326 | /** 327 | * @Then process output should be: 328 | * 329 | * @param PyStringNode $expected 330 | */ 331 | public function processOutputShouldBe(PyStringNode $expected) 332 | { 333 | PHPUnit_Framework_Assert::assertEquals($expected->getRaw(), $this->output); 334 | } 335 | 336 | 337 | /** 338 | * @Given report file should exists 339 | */ 340 | public function reportFileShouldExists() 341 | { 342 | $files = array( 343 | $this->reportDir . 'Index.html', 344 | $this->reportDir . 'assets/Twig/css/style.css', 345 | $this->reportDir . 'assets/Twig/css/style.less', 346 | ); 347 | 348 | foreach ($files as $file) { 349 | PHPUnit_Framework_Assert::assertFileExists($file); 350 | } 351 | } 352 | 353 | /** 354 | * @Given report file should contain: 355 | * 356 | * @param PyStringNode $string 357 | */ 358 | public function reportFileShouldContain(PyStringNode $string) 359 | { 360 | $index = $this->reportDir . 'Index.html'; 361 | 362 | PHPUnit_Framework_Assert::assertContains($string->getRaw(), file_get_contents($index)); 363 | } 364 | 365 | private function getExitCode() 366 | { 367 | return $this->process->getExitCode(); 368 | } 369 | 370 | private function getOutput() 371 | { 372 | $output = $this->process->getErrorOutput() . $this->process->getOutput(); 373 | 374 | // Normalize the line endings in the output 375 | if ("\n" !== PHP_EOL) { 376 | $output = str_replace(PHP_EOL, "\n", $output); 377 | } 378 | 379 | // Replace wrong warning message of HHVM 380 | $output = str_replace('Notice: Undefined index: ', 'Notice: Undefined offset: ', $output); 381 | 382 | return trim(preg_replace("/ +$/m", '', $output)); 383 | } 384 | 385 | private function createFile($filename, $content) 386 | { 387 | $path = dirname($filename); 388 | $this->createDirectory($path); 389 | 390 | file_put_contents($filename, $content); 391 | } 392 | 393 | private function createDirectory($path) 394 | { 395 | if (!is_dir($path)) { 396 | mkdir($path, 0777, true); 397 | } 398 | } 399 | 400 | private function moveToNewPath($path) 401 | { 402 | $newWorkingDir = $this->workingDir . '/' . $path; 403 | if (!file_exists($newWorkingDir)) { 404 | mkdir($newWorkingDir, 0777, true); 405 | } 406 | 407 | $this->workingDir = $newWorkingDir; 408 | } 409 | 410 | private static function clearDirectory($path) 411 | { 412 | $files = scandir($path); 413 | array_shift($files); 414 | array_shift($files); 415 | 416 | foreach ($files as $file) { 417 | $file = $path . DIRECTORY_SEPARATOR . $file; 418 | if (is_dir($file)) { 419 | self::clearDirectory($file); 420 | } else { 421 | unlink($file); 422 | } 423 | } 424 | 425 | rmdir($path); 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /templates/index.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Behat Tests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 51 | 52 |
53 |
54 |
55 |
56 | 57 |
58 |
59 | {{ (failedFeatures|length) + (passedFeatures|length) }} Features: 60 |
61 |
{{ failedFeatures|length }} failed
62 |
63 |
64 |
65 | 66 |
67 |
68 | {{ (failedScenarios|length) + (passedScenarios|length) + (skippedScenarios|length) + (undefinedScenarios|length) }} 69 | Scenarios: 70 |
71 |
{{ failedScenarios|length }} failed
72 |
{{ undefinedScenarios|length }} undefined
73 |
{{ skippedScenarios|length }} skipped
74 |
75 |
76 |
77 | 78 |
79 |
80 | {{ (failedSteps|length) + (passedSteps|length) + (skippedSteps|length) + (undefinedSteps|length) }} 81 | Steps: 82 |
83 |
{{ failedSteps|length }} failed
84 |
{{ undefinedSteps|length }} undefined
85 |
{{ skippedSteps|length }} skipped
86 |
87 |
88 |
89 |
90 | All 91 | Passed 92 | Failed 93 |
94 |
95 |
96 | {% for suite in suites %} 97 |
98 |
99 |

Suite: {{ suite.name }}

100 | 101 |
102 | {% for feature in suite.features %} 103 |
104 |
105 |
106 |

Feature: {{ feature.name }}

107 |
108 |
109 |

{{ feature.description|raw|nl2br }}

110 |
111 | 134 |
135 |
136 | {% endfor %} 137 |
138 |
139 |
140 |
141 | {% endfor %} 142 |
143 |
144 | {% for suite in suites %} 145 | {% for feature in suite.features %} 146 |
147 |
148 |
149 |
150 |

Feature: {{ feature.name }}

151 |
152 |
153 |

{{ feature.description|raw|nl2br }}

154 |
155 | 178 |
179 |
180 |
181 | {% for scenario in feature.scenarios %} 182 |
183 | 202 |
205 |
    206 | {% for step in scenario.steps %} 207 |
  • 208 | {{ step.keyword }} {{ step.text }} 209 | {% if printStepArgs is not null %} 210 | {% for argument in step.arguments %} 211 |

    212 | |{% for subarg in argument %} {{ subarg | nl2br}} | {% endfor %} 213 |

    214 | {% endfor %} 215 | {% endif %} 216 | {% if step.exception is not null %} 217 |
    218 |

    ({{ step.exception }})

    219 | {% endif %} 220 | {% if step.output is not null %} 221 |
    222 |

    | {{ step.output }}

    223 | {% endif %} 224 | {% if printLoopBreak is not null and ( loop.index % scenario.getLoopSize ) == 0 and loop.last != true %} 225 |
  • 226 | {% endif %} 227 |
  • 228 | {#
    #} 229 | {#{{ step.keyword }} {{ step.text }}#} 230 | {#
    #} 231 | {% endfor %} 232 |
233 |
234 |
235 | {% endfor %} 236 |
237 |
238 | {% endfor %} 239 | {% endfor %} 240 |
241 |
242 | 243 | 244 | 245 | 246 | 247 | 248 | 276 | 326 | 327 | 328 | -------------------------------------------------------------------------------- /src/Formatter/BehatHTMLFormatter.php: -------------------------------------------------------------------------------- 1 | 36 | /** 37 | * @var array 38 | */ 39 | private $parameters; 40 | 41 | /** 42 | * @var 43 | */ 44 | private $name; 45 | 46 | /** 47 | * @var 48 | */ 49 | private $timer; 50 | 51 | /** 52 | * @var 53 | */ 54 | private $memory; 55 | 56 | /** 57 | * @param string $outputPath where to save the generated report file 58 | */ 59 | private $outputPath; 60 | 61 | /** 62 | * @param string $base_path Behat base path 63 | */ 64 | private $base_path; 65 | 66 | /** 67 | * Printer used by this Formatter. 68 | * 69 | * @param $printer OutputPrinter 70 | */ 71 | private $printer; 72 | 73 | /** 74 | * Renderer used by this Formatter. 75 | * 76 | * @param $renderer BaseRenderer 77 | */ 78 | private $renderer; 79 | 80 | /** 81 | * Flag used by this Formatter. 82 | * 83 | * @param $print_args boolean 84 | */ 85 | private $print_args; 86 | 87 | /** 88 | * Flag used by this Formatter. 89 | * 90 | * @param $print_outp boolean 91 | */ 92 | private $print_outp; 93 | 94 | /** 95 | * Flag used by this Formatter. 96 | * 97 | * @param $loop_break boolean 98 | */ 99 | private $loop_break; 100 | 101 | /** 102 | * @var array 103 | */ 104 | private $suites; 105 | 106 | /** 107 | * @var Suite 108 | */ 109 | private $currentSuite; 110 | 111 | /** 112 | * @var int 113 | */ 114 | private $featureCounter = 1; 115 | 116 | /** 117 | * @var Feature 118 | */ 119 | private $currentFeature; 120 | 121 | /** 122 | * @var Scenario 123 | */ 124 | private $currentScenario; 125 | 126 | /** 127 | * @var Scenario[] 128 | */ 129 | private $failedScenarios = array(); 130 | 131 | /** 132 | * @var Scenario[] 133 | */ 134 | private $pendingScenarios = array(); 135 | 136 | /** 137 | * @var Scenario[] 138 | */ 139 | private $passedScenarios = array(); 140 | 141 | /** 142 | * @var Feature[] 143 | */ 144 | private $failedFeatures = array(); 145 | 146 | /** 147 | * @var Feature[] 148 | */ 149 | private $passedFeatures = array(); 150 | 151 | /** 152 | * @var Step[] 153 | */ 154 | private $failedSteps = array(); 155 | 156 | /** 157 | * @var Step[] 158 | */ 159 | private $passedSteps = array(); 160 | 161 | /** 162 | * @var Step[] 163 | */ 164 | private $pendingSteps = array(); 165 | 166 | /** 167 | * @var Step[] 168 | */ 169 | private $skippedSteps = array(); 170 | 171 | // 172 | 173 | // 174 | 175 | /** 176 | * @param $name 177 | * @param $base_path 178 | */ 179 | public function __construct($name, $renderer, $filename, $print_args, $print_outp, $loop_break, $base_path) 180 | { 181 | $this->name = $name; 182 | $this->base_path = $base_path; 183 | $this->print_args = $print_args; 184 | $this->print_outp = $print_outp; 185 | $this->loop_break = $loop_break; 186 | $this->renderer = new BaseRenderer($renderer, $base_path); 187 | $this->printer = new FileOutputPrinter($this->renderer->getNameList(), $filename, $base_path); 188 | $this->timer = new Timer(); 189 | $this->memory = new Memory(); 190 | } 191 | 192 | /** 193 | * Returns an array of event names this subscriber wants to listen to. 194 | * 195 | * @return array The event names to listen to 196 | */ 197 | public static function getSubscribedEvents() 198 | { 199 | return array( 200 | 'tester.exercise_completed.before' => 'onBeforeExercise', 201 | 'tester.exercise_completed.after' => 'onAfterExercise', 202 | 'tester.suite_tested.before' => 'onBeforeSuiteTested', 203 | 'tester.suite_tested.after' => 'onAfterSuiteTested', 204 | 'tester.feature_tested.before' => 'onBeforeFeatureTested', 205 | 'tester.feature_tested.after' => 'onAfterFeatureTested', 206 | 'tester.scenario_tested.before' => 'onBeforeScenarioTested', 207 | 'tester.scenario_tested.after' => 'onAfterScenarioTested', 208 | 'tester.outline_tested.before' => 'onBeforeOutlineTested', 209 | 'tester.outline_tested.after' => 'onAfterOutlineTested', 210 | 'tester.step_tested.after' => 'onAfterStepTested', 211 | ); 212 | } 213 | 214 | /** 215 | * Returns formatter name. 216 | * 217 | * @return string 218 | */ 219 | public function getName() 220 | { 221 | return $this->name; 222 | } 223 | 224 | /** 225 | * @return string 226 | */ 227 | public function getBasePath() 228 | { 229 | return $this->base_path; 230 | } 231 | 232 | /** 233 | * Returns formatter description. 234 | * 235 | * @return string 236 | */ 237 | public function getDescription() 238 | { 239 | return 'Formatter for teamcity'; 240 | } 241 | 242 | /** 243 | * Returns formatter output printer. 244 | * 245 | * @return OutputPrinter 246 | */ 247 | public function getOutputPrinter() 248 | { 249 | return $this->printer; 250 | } 251 | 252 | /** 253 | * Sets formatter parameter. 254 | * 255 | * @param string $name 256 | * @param mixed $value 257 | */ 258 | public function setParameter($name, $value) 259 | { 260 | $this->parameters[$name] = $value; 261 | } 262 | 263 | /** 264 | * Returns parameter name. 265 | * 266 | * @param string $name 267 | * 268 | * @return mixed 269 | */ 270 | public function getParameter($name) 271 | { 272 | return $this->parameters[$name]; 273 | } 274 | 275 | /** 276 | * Returns output path. 277 | * 278 | * @return string output path 279 | */ 280 | public function getOutputPath() 281 | { 282 | return $this->printer->getOutputPath(); 283 | } 284 | 285 | /** 286 | * Returns if it should print the step arguments. 287 | * 288 | * @return bool 289 | */ 290 | public function getPrintArguments() 291 | { 292 | return $this->print_args; 293 | } 294 | 295 | /** 296 | * Returns if it should print the step outputs. 297 | * 298 | * @return bool 299 | */ 300 | public function getPrintOutputs() 301 | { 302 | return $this->print_outp; 303 | } 304 | 305 | /** 306 | * Returns if it should print scenario loop break. 307 | * 308 | * @return bool 309 | */ 310 | public function getPrintLoopBreak() 311 | { 312 | return $this->loop_break; 313 | } 314 | 315 | public function getTimer() 316 | { 317 | return $this->timer; 318 | } 319 | 320 | public function getMemory() 321 | { 322 | return $this->memory; 323 | } 324 | 325 | public function getSuites() 326 | { 327 | return $this->suites; 328 | } 329 | 330 | public function getCurrentSuite() 331 | { 332 | return $this->currentSuite; 333 | } 334 | 335 | public function getFeatureCounter() 336 | { 337 | return $this->featureCounter; 338 | } 339 | 340 | public function getCurrentFeature() 341 | { 342 | return $this->currentFeature; 343 | } 344 | 345 | public function getCurrentScenario() 346 | { 347 | return $this->currentScenario; 348 | } 349 | 350 | public function getFailedScenarios() 351 | { 352 | return $this->failedScenarios; 353 | } 354 | 355 | public function getPendingScenarios() 356 | { 357 | return $this->pendingScenarios; 358 | } 359 | 360 | public function getPassedScenarios() 361 | { 362 | return $this->passedScenarios; 363 | } 364 | 365 | public function getFailedFeatures() 366 | { 367 | return $this->failedFeatures; 368 | } 369 | 370 | public function getPassedFeatures() 371 | { 372 | return $this->passedFeatures; 373 | } 374 | 375 | public function getFailedSteps() 376 | { 377 | return $this->failedSteps; 378 | } 379 | 380 | public function getPassedSteps() 381 | { 382 | return $this->passedSteps; 383 | } 384 | 385 | public function getPendingSteps() 386 | { 387 | return $this->pendingSteps; 388 | } 389 | 390 | public function getSkippedSteps() 391 | { 392 | return $this->skippedSteps; 393 | } 394 | 395 | // 396 | 397 | // 398 | 399 | /** 400 | * @param BeforeExerciseCompleted $event 401 | */ 402 | public function onBeforeExercise(BeforeExerciseCompleted $event) 403 | { 404 | $this->timer->start(); 405 | 406 | $print = $this->renderer->renderBeforeExercise($this); 407 | $this->printer->write($print); 408 | } 409 | 410 | /** 411 | * @param AfterExerciseCompleted $event 412 | */ 413 | public function onAfterExercise(AfterExerciseCompleted $event) 414 | { 415 | $this->timer->stop(); 416 | 417 | $print = $this->renderer->renderAfterExercise($this); 418 | $this->printer->writeln($print); 419 | } 420 | 421 | /** 422 | * @param BeforeSuiteTested $event 423 | */ 424 | public function onBeforeSuiteTested(BeforeSuiteTested $event) 425 | { 426 | $this->currentSuite = new Suite(); 427 | $this->currentSuite->setName($event->getSuite()->getName()); 428 | 429 | $print = $this->renderer->renderBeforeSuite($this); 430 | $this->printer->writeln($print); 431 | } 432 | 433 | /** 434 | * @param AfterSuiteTested $event 435 | */ 436 | public function onAfterSuiteTested(AfterSuiteTested $event) 437 | { 438 | $this->suites[] = $this->currentSuite; 439 | 440 | $print = $this->renderer->renderAfterSuite($this); 441 | $this->printer->writeln($print); 442 | } 443 | 444 | /** 445 | * @param BeforeFeatureTested $event 446 | */ 447 | public function onBeforeFeatureTested(BeforeFeatureTested $event) 448 | { 449 | $feature = new Feature(); 450 | $feature->setId($this->featureCounter); 451 | ++$this->featureCounter; 452 | $feature->setName($event->getFeature()->getTitle()); 453 | $feature->setDescription($event->getFeature()->getDescription()); 454 | $feature->setTags($event->getFeature()->getTags()); 455 | $feature->setFile($event->getFeature()->getFile()); 456 | $feature->setScreenshotFolder($event->getFeature()->getTitle()); 457 | $this->currentFeature = $feature; 458 | 459 | $print = $this->renderer->renderBeforeFeature($this); 460 | $this->printer->writeln($print); 461 | } 462 | 463 | /** 464 | * @param AfterFeatureTested $event 465 | */ 466 | public function onAfterFeatureTested(AfterFeatureTested $event) 467 | { 468 | $this->currentSuite->addFeature($this->currentFeature); 469 | if ($this->currentFeature->allPassed()) { 470 | $this->passedFeatures[] = $this->currentFeature; 471 | } else { 472 | $this->failedFeatures[] = $this->currentFeature; 473 | } 474 | 475 | $print = $this->renderer->renderAfterFeature($this); 476 | $this->printer->writeln($print); 477 | } 478 | 479 | /** 480 | * @param BeforeScenarioTested $event 481 | */ 482 | public function onBeforeScenarioTested(BeforeScenarioTested $event) 483 | { 484 | $scenario = new Scenario(); 485 | $scenario->setName($event->getScenario()->getTitle()); 486 | $scenario->setTags($event->getScenario()->getTags()); 487 | $scenario->setLine($event->getScenario()->getLine()); 488 | $scenario->setScreenshotName($event->getScenario()->getTitle()); 489 | $scenario->setScreenshotPath( 490 | $this->printer->getOutputPath(). 491 | '/assets/screenshots/'. 492 | preg_replace('/\W/', '', $event->getFeature()->getTitle()).'/'. 493 | preg_replace('/\W/', '', $event->getScenario()->getTitle()).'.png' 494 | ); 495 | $this->currentScenario = $scenario; 496 | 497 | $print = $this->renderer->renderBeforeScenario($this); 498 | $this->printer->writeln($print); 499 | } 500 | 501 | /** 502 | * @param AfterScenarioTested $event 503 | */ 504 | public function onAfterScenarioTested(AfterScenarioTested $event) 505 | { 506 | $scenarioPassed = $event->getTestResult()->isPassed(); 507 | 508 | if ($scenarioPassed) { 509 | $this->passedScenarios[] = $this->currentScenario; 510 | $this->currentFeature->addPassedScenario(); 511 | $this->currentScenario->setPassed(true); 512 | } elseif (StepResult::PENDING == $event->getTestResult()->getResultCode()) { 513 | $this->pendingScenarios[] = $this->currentScenario; 514 | $this->currentFeature->addPendingScenario(); 515 | $this->currentScenario->setPending(true); 516 | } else { 517 | $this->failedScenarios[] = $this->currentScenario; 518 | $this->currentFeature->addFailedScenario(); 519 | $this->currentScenario->setPassed(false); 520 | $this->currentScenario->setPending(false); 521 | } 522 | 523 | $this->currentScenario->setLoopCount(1); 524 | $this->currentFeature->addScenario($this->currentScenario); 525 | 526 | $print = $this->renderer->renderAfterScenario($this); 527 | $this->printer->writeln($print); 528 | } 529 | 530 | /** 531 | * @param BeforeOutlineTested $event 532 | */ 533 | public function onBeforeOutlineTested(BeforeOutlineTested $event) 534 | { 535 | $scenario = new Scenario(); 536 | $scenario->setName($event->getOutline()->getTitle()); 537 | $scenario->setTags($event->getOutline()->getTags()); 538 | $scenario->setLine($event->getOutline()->getLine()); 539 | $this->currentScenario = $scenario; 540 | 541 | $print = $this->renderer->renderBeforeOutline($this); 542 | $this->printer->writeln($print); 543 | } 544 | 545 | /** 546 | * @param AfterOutlineTested $event 547 | */ 548 | public function onAfterOutlineTested(AfterOutlineTested $event) 549 | { 550 | $scenarioPassed = $event->getTestResult()->isPassed(); 551 | 552 | if ($scenarioPassed) { 553 | $this->passedScenarios[] = $this->currentScenario; 554 | $this->currentFeature->addPassedScenario(); 555 | $this->currentScenario->setPassed(true); 556 | } elseif (StepResult::PENDING == $event->getTestResult()->getResultCode()) { 557 | $this->pendingScenarios[] = $this->currentScenario; 558 | $this->currentFeature->addPendingScenario(); 559 | $this->currentScenario->setPending(true); 560 | } else { 561 | $this->failedScenarios[] = $this->currentScenario; 562 | $this->currentFeature->addFailedScenario(); 563 | $this->currentScenario->setPassed(false); 564 | $this->currentScenario->setPending(false); 565 | } 566 | 567 | $this->currentScenario->setLoopCount(sizeof($event->getTestResult())); 568 | $this->currentFeature->addScenario($this->currentScenario); 569 | 570 | $print = $this->renderer->renderAfterOutline($this); 571 | $this->printer->writeln($print); 572 | } 573 | 574 | /** 575 | * @param BeforeStepTested $event 576 | */ 577 | public function onBeforeStepTested(BeforeStepTested $event) 578 | { 579 | $print = $this->renderer->renderBeforeStep($this); 580 | $this->printer->writeln($print); 581 | } 582 | 583 | /** 584 | * @param AfterStepTested $event 585 | */ 586 | public function onAfterStepTested(AfterStepTested $event) 587 | { 588 | $result = $event->getTestResult(); 589 | 590 | /** @var Step $step */ 591 | $step = new Step(); 592 | $step->setKeyword($event->getStep()->getKeyword()); 593 | $step->setText($event->getStep()->getText()); 594 | $step->setLine($event->getStep()->getLine()); 595 | $step->setResult($result); 596 | $step->setResultCode($result->getResultCode()); 597 | 598 | if ($event->getStep()->hasArguments()) { 599 | $object = $this->getObject($event->getStep()->getArguments()); 600 | $step->setArgumentType($object->getNodeType()); 601 | $step->setArguments($object); 602 | } 603 | 604 | //What is the result of this step ? 605 | if (is_a($result, 'Behat\Behat\Tester\Result\UndefinedStepResult')) { 606 | //pending step -> no definition to load 607 | $this->pendingSteps[] = $step; 608 | } else { 609 | if (is_a($result, 'Behat\Behat\Tester\Result\SkippedStepResult')) { 610 | //skipped step 611 | /* @var ExecutedStepResult $result */ 612 | $step->setDefinition($result->getStepDefinition()); 613 | $this->skippedSteps[] = $step; 614 | } else { 615 | //failed or passed 616 | if ($result instanceof ExecutedStepResult) { 617 | $step->setDefinition($result->getStepDefinition()); 618 | $exception = $result->getException(); 619 | if ($exception) { 620 | if ($exception instanceof PendingException) { 621 | $this->pendingSteps[] = $step; 622 | } else { 623 | $step->setException($exception->getMessage()); 624 | $this->failedSteps[] = $step; 625 | } 626 | } else { 627 | $step->setOutput($result->getCallResult()->getStdOut()); 628 | $this->passedSteps[] = $step; 629 | } 630 | } 631 | } 632 | } 633 | 634 | $this->currentScenario->addStep($step); 635 | 636 | $print = $this->renderer->renderAfterStep($this); 637 | $this->printer->writeln($print); 638 | } 639 | 640 | // 641 | 642 | /** 643 | * @param $arguments 644 | */ 645 | public function getObject($arguments) 646 | { 647 | foreach ($arguments as $argument => $args) { 648 | return $args; 649 | } 650 | } 651 | } 652 | -------------------------------------------------------------------------------- /src/Renderer/Behat2Renderer.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace emuse\BehatHTMLFormatter\Renderer; 9 | 10 | use Behat\Gherkin\Node\TableNode; 11 | 12 | class Behat2Renderer implements RendererInterface 13 | { 14 | /** 15 | * Renders before an exercice. 16 | * 17 | * @param object : BehatHTMLFormatter object 18 | * 19 | * @return string : HTML generated 20 | */ 21 | public function renderBeforeExercise($obj) 22 | { 23 | $print = " 24 | 25 | 26 | 27 | Behat Test Suite ".$this->getCSS()." 28 | 29 | 30 |
"; 31 | 32 | return $print; 33 | } 34 | 35 | /** 36 | * Renders after an exercice. 37 | * 38 | * @param object : BehatHTMLFormatter object 39 | * 40 | * @return string : HTML generated 41 | */ 42 | public function renderAfterExercise($obj) 43 | { 44 | //--> features results 45 | $featTotal = 0; 46 | $sceTotal = 0; 47 | $stepsTotal = 0; 48 | 49 | $strFeatPassed = ''; 50 | 51 | if (null !== $obj->getPassedFeatures() && count($obj->getPassedFeatures()) > 0) { 52 | $strFeatPassed = ' '.count($obj->getPassedFeatures()).' success'; 53 | $featTotal += count($obj->getPassedFeatures()); 54 | } 55 | 56 | $strFeatFailed = ''; 57 | $sumRes = 'passed'; 58 | if (null !== $obj->getFailedFeatures() && count($obj->getFailedFeatures()) > 0) { 59 | $strFeatFailed = ' '.count($obj->getFailedFeatures()).' fail'; 60 | $sumRes = 'failed'; 61 | $featTotal += count($obj->getFailedFeatures()); 62 | } 63 | 64 | //--> scenarios results 65 | $strScePassed = ''; 66 | if (null !== $obj->getPassedScenarios() && count($obj->getPassedScenarios()) > 0) { 67 | $strScePassed = ' '.count($obj->getPassedScenarios()).' success'; 68 | $sceTotal += count($obj->getPassedScenarios()); 69 | } 70 | 71 | $strScePending = ''; 72 | if (null !== $obj->getPendingScenarios() && count($obj->getPendingScenarios()) > 0) { 73 | $strScePending = ' '.count($obj->getPendingScenarios()).' fail'; 74 | $sceTotal += count($obj->getPendingScenarios()); 75 | } 76 | 77 | $strSceFailed = ''; 78 | if (null !== $obj->getFailedScenarios() && count($obj->getFailedScenarios()) > 0) { 79 | $strSceFailed = ' '.count($obj->getFailedScenarios()).' fail'; 80 | $sceTotal += count($obj->getFailedScenarios()); 81 | } 82 | 83 | //--> steps results 84 | $strStepsPassed = ''; 85 | if (null !== $obj->getPassedSteps() && count($obj->getPassedSteps()) > 0) { 86 | $strStepsPassed = ' '.count($obj->getPassedSteps()).' success'; 87 | $stepsTotal += count($obj->getPassedSteps()); 88 | } 89 | 90 | $strStepsPending = ''; 91 | if (null !== $obj->getPendingSteps() && count($obj->getPendingSteps()) > 0) { 92 | $strStepsPending = ' '.count($obj->getPendingSteps()).' pending'; 93 | $stepsTotal += count($obj->getPendingSteps()); 94 | } 95 | 96 | $strStepsSkipped = ''; 97 | if (null !== $obj->getSkippedSteps() && count($obj->getSkippedSteps()) > 0) { 98 | $strStepsSkipped = ' '.count($obj->getSkippedSteps()).' skipped'; 99 | $stepsTotal += count($obj->getSkippedSteps()); 100 | } 101 | 102 | $strStepsFailed = ''; 103 | if (null !== $obj->getFailedSteps() && count($obj->getFailedSteps()) > 0) { 104 | $strStepsFailed = ' '.count($obj->getFailedSteps()).' fail'; 105 | $stepsTotal += count($obj->getFailedSteps()); 106 | } 107 | 108 | //list of pending steps to display 109 | $strPendingList = ''; 110 | if (null !== $obj->getPendingSteps() && count($obj->getPendingSteps()) > 0) { 111 | foreach ($obj->getPendingSteps() as $pendingStep) { 112 | $strPendingList .= ' 113 |
  • '.$pendingStep->getKeyword().' '.$pendingStep->getText().'
  • '; 114 | } 115 | $strPendingList = ' 116 |
    Pending steps : 117 |
      '.$strPendingList.' 118 |
    119 |
    '; 120 | } 121 | 122 | $print = ' 123 |
    124 |
    125 |

    126 | '.$featTotal.' features ('.$strFeatPassed.$strFeatFailed.' ) 127 |

    128 |

    129 | '.$sceTotal.' scenarios ('.$strScePassed.$strScePending.$strSceFailed.' ) 130 |

    131 |

    132 | '.$stepsTotal.' steps ('.$strStepsPassed.$strStepsPending.$strStepsSkipped.$strStepsFailed.' ) 133 |

    134 |

    135 | '.$obj->getTimer().' - '.$obj->getMemory().' 136 |

    137 |
    138 |
    139 | [+] all 140 | [-] all 141 |
    142 |
    '.$strPendingList.' 143 |
    '.$this->getJS().' 144 | 145 | '; 146 | 147 | return $print; 148 | } 149 | 150 | /** 151 | * Renders before a suite. 152 | * 153 | * @param object : BehatHTMLFormatter object 154 | * 155 | * @return string : HTML generated 156 | */ 157 | public function renderBeforeSuite($obj) 158 | { 159 | $print = ' 160 |
    Suite : '.$obj->getCurrentSuite()->getName().'
    '; 161 | 162 | return $print; 163 | } 164 | 165 | /** 166 | * Renders after a suite. 167 | * 168 | * @param object : BehatHTMLFormatter object 169 | * 170 | * @return string : HTML generated 171 | */ 172 | public function renderAfterSuite($obj) 173 | { 174 | return ''; 175 | } 176 | 177 | /** 178 | * Renders before a feature. 179 | * 180 | * @param object : BehatHTMLFormatter object 181 | * 182 | * @return string : HTML generated 183 | */ 184 | public function renderBeforeFeature($obj) 185 | { 186 | //feature head 187 | $print = ' 188 |
    189 |

    190 | Feature: 191 | '.$obj->getCurrentFeature()->getName().' 192 |

    193 |

    '.$obj->getCurrentFeature()->getDescription().'

    194 |
      '; 195 | foreach ($obj->getCurrentFeature()->getTags() as $tag) { 196 | $print .= ' 197 |
    • @'.$tag.'
    • '; 198 | } 199 | $print .= ' 200 |
    '; 201 | 202 | //TODO path is missing (?) 203 | 204 | return $print; 205 | } 206 | 207 | /** 208 | * Renders after a feature. 209 | * 210 | * @param object : BehatHTMLFormatter object 211 | * 212 | * @return string : HTML generated 213 | */ 214 | public function renderAfterFeature($obj) 215 | { 216 | //list of results 217 | $print = ' 218 |
    Feature has '.$obj->getCurrentFeature()->getPassedClass(); 219 | 220 | //percent only if failed scenarios 221 | if ($obj->getCurrentFeature()->getTotalAmountOfScenarios() > 0 && 'failed' === $obj->getCurrentFeature()->getPassedClass()) { 222 | $print .= ' 223 | Scenarios passed : '.round($obj->getCurrentFeature()->getPercentPassed(), 2).'%, 224 | Scenarios failed : '.round($obj->getCurrentFeature()->getPercentFailed(), 2).'%'; 225 | } 226 | 227 | $print .= ' 228 |
    229 |
    '; 230 | 231 | return $print; 232 | } 233 | 234 | /** 235 | * Renders before a scenario. 236 | * 237 | * @param object : BehatHTMLFormatter object 238 | * 239 | * @return string : HTML generated 240 | */ 241 | public function renderBeforeScenario($obj) 242 | { 243 | //scenario head 244 | $print = ' 245 |
    246 |
      '; 247 | foreach ($obj->getCurrentScenario()->getTags() as $tag) { 248 | $print .= ' 249 |
    • @'.$tag.'
    • '; 250 | } 251 | $print .= ' 252 |
    '; 253 | 254 | $print .= ' 255 |

    256 | '.$obj->getCurrentScenario()->getId().' Scenario: 257 | '.$obj->getCurrentScenario()->getName().' 258 |

    259 |
      '; 260 | 261 | //TODO path is missing 262 | 263 | return $print; 264 | } 265 | 266 | /** 267 | * Renders after a scenario. 268 | * 269 | * @param object : BehatHTMLFormatter object 270 | * 271 | * @return string : HTML generated 272 | */ 273 | public function renderAfterScenario($obj) 274 | { 275 | $print = ' 276 |
    277 |
    '; 278 | 279 | return $print; 280 | } 281 | 282 | /** 283 | * Renders before an outline. 284 | * 285 | * @param object : BehatHTMLFormatter object 286 | * 287 | * @return string : HTML generated 288 | */ 289 | public function renderBeforeOutline($obj) 290 | { 291 | //scenario head 292 | $print = ' 293 |
    294 |
      '; 295 | foreach ($obj->getCurrentScenario()->getTags() as $tag) { 296 | $print .= ' 297 |
    • @'.$tag.'
    • '; 298 | } 299 | $print .= ' 300 |
    '; 301 | 302 | $print .= ' 303 |

    304 | '.$obj->getCurrentScenario()->getId().' Scenario Outline: 305 | '.$obj->getCurrentScenario()->getName().' 306 |

    307 |
      '; 308 | 309 | //TODO path is missing 310 | 311 | return $print; 312 | } 313 | 314 | /** 315 | * Renders after an outline. 316 | * 317 | * @param object : BehatHTMLFormatter object 318 | * 319 | * @return string : HTML generated 320 | */ 321 | public function renderAfterOutline($obj) 322 | { 323 | return $this->renderAfterScenario($obj); 324 | } 325 | 326 | /** 327 | * Renders before a step. 328 | * 329 | * @param object : BehatHTMLFormatter object 330 | * 331 | * @return string : HTML generated 332 | */ 333 | public function renderBeforeStep($obj) 334 | { 335 | return ''; 336 | } 337 | 338 | /** 339 | * Renders TableNode arguments. 340 | * 341 | * @param TableNode $table 342 | * 343 | * @return string : HTML generated 344 | */ 345 | public function renderTableNode(TableNode $table) 346 | { 347 | $arguments = ''; 348 | $header = $table->getRow(0); 349 | $arguments .= $this->preintTableRows($header); 350 | 351 | $arguments .= ''; 352 | foreach ($table->getHash() as $row) { 353 | $arguments .= $this->preintTableRows($row); 354 | } 355 | 356 | $arguments .= '
      '; 357 | 358 | return $arguments; 359 | } 360 | 361 | /** 362 | * Renders table rows. 363 | * 364 | * @param array $row 365 | * 366 | * @return string : HTML generated 367 | */ 368 | public function preintTableRows($row) 369 | { 370 | $return = ''; 371 | foreach ($row as $column) { 372 | $return .= ''.htmlentities($column).''; 373 | } 374 | $return .= ''; 375 | 376 | return $return; 377 | } 378 | 379 | /** 380 | * Renders after a step. 381 | * 382 | * @param object : BehatHTMLFormatter object 383 | * 384 | * @return string : HTML generated 385 | */ 386 | public function renderAfterStep($obj) 387 | { 388 | $feature = $obj->getCurrentFeature(); 389 | $scenario = $obj->getCurrentScenario(); 390 | 391 | $steps = $scenario->getSteps(); 392 | $step = end($steps); //needed because of strict standards 393 | 394 | //path displayed only if available (it's not available in undefined steps) 395 | $strPath = ''; 396 | if (null !== $step->getDefinition()) { 397 | $strPath = $step->getDefinition()->getPath(); 398 | } 399 | 400 | $stepResultClass = ''; 401 | if ($step->isPassed()) { 402 | $stepResultClass = 'passed'; 403 | } 404 | if ($step->isFailed()) { 405 | $stepResultClass = 'failed'; 406 | } 407 | if ($step->isSkipped()) { 408 | $stepResultClass = 'skipped'; 409 | } 410 | if ($step->isPending()) { 411 | $stepResultClass = 'pending'; 412 | } 413 | 414 | $arguments = ''; 415 | $argumentType = $step->getArgumentType(); 416 | 417 | if ('PyString' == $argumentType) { 418 | $arguments = '
      '.htmlentities($step->getArguments()).'
      '; 419 | } 420 | 421 | if ('Table' == $argumentType) { 422 | $arguments = '
      '.$this->renderTableNode($step->getArguments()).'
      '; 423 | } 424 | 425 | $print = ' 426 |
    1. 427 |
      428 | '.$step->getKeyWord().' 429 | '.htmlentities($step->getText()).' 430 | '.$strPath.'' 431 | .$arguments.' 432 |
      '; 433 | $exception = $step->getException(); 434 | if (!empty($exception)) { 435 | $print .= ' 436 |
      '.$step->getException().'
      '; 437 | if ($scenario->getRelativeScreenshotPath()) { 438 | $print .= 'Screenshot'; 439 | } 440 | } 441 | $print .= ' 442 |
    2. '; 443 | 444 | return $print; 445 | } 446 | 447 | /** 448 | * To include CSS. 449 | * 450 | * @return string : HTML generated 451 | */ 452 | public function getCSS() 453 | { 454 | return " 727 | 728 | "; 776 | } 777 | 778 | /** 779 | * To include JS. 780 | * 781 | * @return string : HTML generated 782 | */ 783 | public function getJS() 784 | { 785 | return " 786 | "; 884 | } 885 | } 886 | --------------------------------------------------------------------------------