├── 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 | [](https://packagist.org/packages/emuse/behat-html-formatter) [](https://packagist.org/packages/emuse/behat-html-formatter) [](https://packagist.org/packages/emuse/behat-html-formatter) [](https://packagist.org/packages/emuse/behat-html-formatter)
6 |
7 | ### Twig report
8 |
9 | 
10 |
11 | ### Behat 2 report
12 |
13 | 
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 |
94 |
95 |
96 | {% for suite in suites %}
97 |
98 |
99 |
Suite: {{ suite.name }}
100 |
101 |
102 | {% for feature in suite.features %}
103 |
104 |
105 |
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 |
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 |
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 |
';
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 |
';
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 |
';
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 | -
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 | ';
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 |
--------------------------------------------------------------------------------