├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── --bug-report.md │ ├── --documentation.md │ ├── --feature-request.md │ └── --question.md └── stale.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── app_features │ └── app.feature ├── config │ ├── wdio.app.android.conf.js │ ├── wdio.app.ios.conf.js │ ├── wdio.browsers.conf.js │ └── wdio.shared.conf.js ├── features │ ├── ambiguous.feature │ ├── attach.feature │ ├── data.feature │ ├── failed.feature │ ├── issue-16.feature │ ├── no.scenario.feature │ ├── outline.feature │ ├── passed.feature │ ├── pending.feature │ ├── skipped.feature │ └── undefined.feature └── step_definitions │ └── all.steps.js ├── docs ├── CONTRIBUTING.md └── assets │ ├── before-after.jpg │ ├── custom-data.jpg │ ├── feature-overview.jpg │ ├── features-overview.jpg │ ├── given-datatables.jpg │ └── report-overview.jpg ├── lib ├── generateJson.js └── reporter.js ├── package-lock.json └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ], 5 | "plugins": [ 6 | "transform-function-bind", 7 | "transform-object-rest-spread", 8 | "add-module-exports" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 4 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [{.babelrc,.eslintrc,.travis.yml}] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "standard", 4 | "plugins": [ 5 | "mocha" 6 | ], 7 | "rules": { 8 | "indent": [2, 4], 9 | "mocha/no-exclusive-tests": "error" 10 | }, 11 | "globals": { 12 | "browser": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41BBug report" 3 | about: Create a report to help us improve. 4 | 5 | --- 6 | 7 | **Environment (please complete the following information):** 8 | - Node.js version: [e.g. 8.9.1] 9 | - NPM version: [e.g. 5.8.0] 10 | - webdriver.io version: [e.g. 4.13.1] 11 | - wdio-cucumber-framework version: [e.g. 2.2.0 (if lower this module will not work as it should be)] 12 | - wdio-multiple-cucumber-html-reporter version: [e.g. 0.1.3] 13 | 14 | **Config of webdriver.io and the reporter** 15 | An example of how you configured the reporter in your webdriver.io config 16 | 17 | **Describe the bug** 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | Steps to reproduce the behavior: 22 | 23 | [Include code or an example repository that can easily be set up] 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Snapshots** 29 | If applicable, add snapshots to help explain your problem. 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4D6Documentation" 3 | about: Suggest improvements or report missing/unclear documentation. 4 | 5 | --- 6 | 7 | **Pre-check** 8 | - [ ] I'm aware that I can [edit the docs](https://github.com/wswebcreation/wdio-multiple-cucumber-html-reporter) and submit a pull request 9 | 10 | **Describe the improvement** 11 | 12 | I'd like to report 13 | - [ ] Unclear documentation 14 | - [ ] A typo 15 | - [ ] Missing documentation 16 | - [ ] Other 17 | 18 | **Description of the improvement / report** 19 | A clear and concise description. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1Feature request" 3 | about: Suggest an idea for this module. 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4ACQuestion" 3 | about: Ask questions. 4 | 5 | --- 6 | 7 | **Describe your question with as much detail as possible** 8 | A clear and concise question that doesn't require too much conversation. Need more help? [Find me on Gitter](https://gitter.im/wswebcreation) 9 | 10 | 11 | **If it's about a specific piece of code, try and include some of it to support your question.** 12 | [...] 13 | 14 | 15 | **Environment (please complete the following information):** 16 | - Node.js version: [e.g. 8.9.1] 17 | - NPM version: [e.g. 5.8.0] 18 | - webdriver.io version: [e.g. 4.13.1] 19 | - wdio-cucumber-framework version: [e.g. 2.2.0 (if lower this module will not work as it should be)] 20 | - wdio-multiple-cucumber-html-reporter version: [e.g. 0.1.3] 21 | 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - bug 8 | # Label to use when marking an issue as stale 9 | staleLabel: wontfix 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale because it has not had 13 | recent activity. It will be closed if no further activity occurs. Thank you 14 | for your contributions. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Directory for instrumented libs generated by jscoverage/JSCover 6 | lib-cov 7 | 8 | # Coverage directory used by tools like istanbul 9 | coverage 10 | 11 | # Compiled binary addons (http://nodejs.org/api/addons.html) 12 | build 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # JetBrains IDE 19 | .idea/ 20 | .tmp/ 21 | .dist/ 22 | build/ 23 | 24 | #Visual Studio Code 25 | .DS_Store 26 | 27 | package-lock.json 28 | 29 | apps/ 30 | 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Directory for instrumented libs generated by jscoverage/JSCover 6 | lib-cov 7 | 8 | # Coverage directory used by tools like istanbul 9 | coverage 10 | 11 | # Dependency directory 12 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 13 | node_modules 14 | 15 | # JetBrains IDE 16 | .idea 17 | 18 | package-lock.json 19 | 20 | .tmp/ 21 | .github/ 22 | __tests__/ 23 | apps/ 24 | docs/ 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | See [Releases](https://github.com/wswebcreation/wdio-multiple-cucumber-html-reporter/releases) 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wim Selles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wdio-multiple-cucumber-html-reporter 2 | A WebdriverIO reporter plugin. Reporter that creates beautiful Cucumber HTML reports by using [multiple-cucumber-html-reporter](https://github.com/wswebcreation/multiple-cucumber-html-reporter) 3 | 4 | > **THIS MODULE IS DEPRECATED AND IS NOT MAINTAINED ANYMORE. IT DOESN'T SUPPORT CUCUMBERJS WITH WEBDRIVERIO V5 AND IT CAN'T BE MIGRATED TO V5 DUE TO TECHNICAL LIMITATIONS** 5 | > **IF YOU WANT TO USE CUCUMBERJS WITH WEBDRIVERIO V5 AND A SIMILAR IMPLEMENTATION LIKE THIS, PLEASE CHECK [wdio-cucumberjs-json-reporter](https://github.com/wswebcreation/wdio-cucumberjs-json-reporter)** 6 | 7 | 8 | > **~THIS REPORTER IS STILL UNDER DEVELOPMENT, FEEL FREE TO ADD PR'S~** 9 | 10 | > **THIS REPORTER NEEDS TO USE `wdio-cucumber-framework` `2.2.5` OR HIGHER, SEE FAQ'S** 11 | 12 | > **`multiple-cucumber-html-reporter`, WHICH IS USED IN THIS REPORTER PLUGIN WILL ONLY WORK ON NODE >= 7** 13 | 14 | > **THIS MODULE CAN ONLY WORK WITH WebdriverIO V4 BECAUSE `wdio-cucumber-framework` IS NOT V5 COMPATIBLE** 15 | 16 | ## What does it do? 17 | This reporter does two things 18 | 19 | 1. First of all it creates a CucumberJS-JSON file. This is needed because the BaseReporter of webdriver.io overwrites the CucumberJS report output 20 | 2. Based on the created JSON it creates a HTML-report like below 21 | 22 | ![Snapshot - Report overview](./docs/assets/report-overview.jpg) 23 | 24 | 3. It also supports `Given|When|Then`-keywords and Datatables 25 | 26 | ![Snapshot - Given When Then Datatables](./docs/assets/given-datatables.jpg) 27 | 28 | 4. Data can also be added to **steps** (so not `before|after`-hooks) with the following code 29 | 30 | 31 | ```js 32 | import multipleCucumberHtmlReporter from 'wdio-multiple-cucumber-html-reporter'; 33 | 34 | // Attach a string 35 | multipleCucumberHtmlReporter.attach('just a string'); 36 | // Attach JSON 37 | multipleCucumberHtmlReporter.attach({"json-string": true}, 'application/json'); 38 | // Attach a screenshot 39 | multipleCucumberHtmlReporter.attach(browser.saveScreenshot(), 'image/png'); 40 | // Or with 41 | multipleCucumberHtmlReporter.attach(browser.screenshot(), 'image/png'); 42 | ``` 43 | 44 | 5. Data can also be added to **`before|after`**-hooks. You can use the same method as above 45 | but you need to add an extra `hookType`, which is a string. For the `before`-hook you use the string `before` and for the `after`-hook you can use the string `after`. 46 | See the examples below 47 | 48 | > **Keep in mind this is only for the `before|after`-hooks, if you add them in steps you will mess up your own report** 49 | 50 | ```js 51 | import multipleCucumberHtmlReporter from 'wdio-multiple-cucumber-html-reporter'; 52 | 53 | // BEFORE 54 | // Attach a string in a before hook 55 | multipleCucumberHtmlReporter.attach('just a string', 'before'); 56 | // Attach JSON in a before hook 57 | multipleCucumberHtmlReporter.attach({"json-string": true}, 'application/json', 'before'); 58 | // Attach a screenshot in a before hook 59 | multipleCucumberHtmlReporter.attach(browser.saveScreenshot(), 'image/png', 'before'); 60 | // Or with 61 | multipleCucumberHtmlReporter.attach(browser.screenshot(), 'image/png', 'before'); 62 | 63 | 64 | // AFTER 65 | // Attach a string in a after hook 66 | multipleCucumberHtmlReporter.attach('just a string', 'after'); 67 | // Attach JSON in a after hook 68 | multipleCucumberHtmlReporter.attach({"json-string": true}, 'application/json', 'after'); 69 | // Attach a screenshot in a after hook 70 | multipleCucumberHtmlReporter.attach(browser.saveScreenshot(), 'image/png', 'after'); 71 | // Or with 72 | multipleCucumberHtmlReporter.attach(browser.screenshot(), 'image/png', 'after'); 73 | ``` 74 | 75 | Keep in mind that this will add a passed step for the `before|after`-hook as can be seen here 76 | 77 | ![Snapshot - Before / After hook](./docs/assets/before-after.jpg) 78 | 79 | > Not all options / data that is provided in [multiple-cucumber-html-reporter](https://github.com/wswebcreation/multiple-cucumber-html-reporter) can be used due to limitations in the generated JSON file by this reporter 80 | 81 | 82 | ## Installation 83 | The easiest way is to keep `wdio-multiple-cucumber-html-reporter` as a devDependency in your `package.json`. 84 | 85 | ```json 86 | { 87 | "devDependencies": { 88 | "wdio-multiple-cucumber-html-reporter": "^0.2.0" 89 | } 90 | } 91 | ``` 92 | 93 | You can simple do it by: 94 | 95 | ```bash 96 | npm install wdio-multiple-cucumber-html-reporter --save-dev 97 | ``` 98 | 99 | so it will be added automatically to your `package.json` 100 | 101 | Instructions on how to install `WebdriverIO` can be found [here](http://webdriver.io/guide/getstarted/install.html). 102 | 103 | ## Configuration 104 | Configure the output directory in your wdio.conf.js file: 105 | 106 | ```js 107 | exports.config = { 108 | // ... 109 | reporters: ['multiple-cucumber-html'], 110 | reporterOptions: { 111 | htmlReporter: { 112 | jsonFolder: './tmp', 113 | reportFolder: `./tmp/report`, 114 | // ... other options, see Options 115 | } 116 | }, 117 | // ... 118 | } 119 | ``` 120 | 121 | ## Options 122 | ### `jsonFolder` 123 | - **Type:** `String` 124 | - **Mandatory:** Yes 125 | 126 | The directory where the JSON file, generated by this report, will be stored, relative from where the script is started. 127 | 128 | **N.B.:** If you use a npm script from the command line, like for example `npm run test` the `jsonFolder` will be relative from the path where the script is executed. Executing it from the root of your project will also create the `jsonFolder` in the root of you project. 129 | 130 | ### `reportFolder` 131 | - **Type:** `String` 132 | - **Mandatory:** Yes 133 | 134 | The directory in which the report needs to be saved, relative from where the script is started. 135 | 136 | **N.B.:** If you use a npm script from the command line, like for example `npm run test` the `reportFolder` will be relative from the path where the script is executed. Executing it from the root of your project will also save the report in the `reportFolder` in the root of you project. 137 | 138 | ### `removeFolders` 139 | - **Type:** `boolean` 140 | - **Mandatory:** No 141 | 142 | If `true` the the `jsonFolder` and the `reportFolder` will be removed to start the test with a clean state. 143 | 144 | ### `openReportInBrowser` 145 | - **Type:** `boolean` 146 | - **Mandatory:** No 147 | 148 | If `true` the report will automatically be opened in the default browser of the operating system. 149 | 150 | ### `saveCollectedJSON` 151 | - **Type:** `boolean` 152 | - **Mandatory:** No 153 | 154 | This reporter will first get the JSON-file and then enrich it with data that is used for the report. If `saveCollectedJSON :true` the JSON **AND** the enriched JSON will be saved in the `reportFolder`. They will be saved as: 155 | 156 | - `merged-output.json` 157 | - `enriched-output.json` 158 | 159 | ### `disableLog` 160 | - **Type:** `boolean` 161 | - **Mandatory:** No 162 | - **Default:** `false` 163 | 164 | This will disable the log so will **NOT** see this. 165 | 166 | ```shell 167 | ===================================================================================== 168 | Multiple Cucumber HTML report generated in: 169 | 170 | /Users/wswebcreation/multiple-cucumber-html-reporter/.tmp/index.html 171 | ======================================================================== 172 | ``` 173 | ### `pageTitle` 174 | - **Type:** `string` 175 | - **Mandatory:** No 176 | - **Default:** Multiple Cucumber HTML Reporter 177 | 178 | You can change the report title in the HTML head Tag 179 | 180 | ### `reportName` 181 | - **Type:** `string` 182 | - **Mandatory:** No 183 | 184 | You can change the report name to a name you want 185 | 186 | ### `pageFooter` 187 | - **Type:** `string` 188 | - **Mandatory:** No 189 | 190 | You can customise Page Footer if required. You just need to provide a html string like `

A custom footer in html

` 191 | 192 | ### `displayDuration` 193 | - **Type:** `boolean` 194 | - **Mandatory:** No 195 | 196 | If set to `true` the duration of steps, scenarios and features is displayed on the Features overview and single feature page in an easily readable format. 197 | This expects the durations in the report to be in **nanoseconds**, which might result in incorrect durations when using a version of Cucumber(JS 2 and 3) that does not report in nanoseconds but in milliseconds. This can be changed to milliseconds by adding the parameter `durationInMS: true`, see below 198 | 199 | > **NOTE: Only the duration of a feature can be shown in the features overview. A total duration over all features CAN NOT be given because the module doesn't know if all features have been run in parallel** 200 | 201 | ### `durationInMS` 202 | - **Type:** `boolean` 203 | - **Default:** `false` 204 | - **Mandatory:** No 205 | 206 | If set to `true` the duration of steps will be expected to be in **milliseconds**, which might result in incorrect durations when using a version of Cucumber(JS 1 or 4) that does report in **nanaseconds**. 207 | This parameter relies on `displayDuration: true` 208 | 209 | ### `customStyle` 210 | - **Type:** `path` 211 | - **Mandatory:** No 212 | 213 | If you need add some custom style to your report. Add it like this `customStyle: 'your-path-where/custom.css'` 214 | 215 | ### `overrideStyle` 216 | - **Type:** `path` 217 | - **Mandatory:** No 218 | 219 | If you need replace default style for your report. Add it like this `overrideStyle: 'your-path-where/custom.css'` 220 | 221 | ### `customData` 222 | - **Type:** `object` 223 | - **Mandatory:** No 224 | 225 | You can add a custom data block to the report like this 226 | 227 | ![Snapshot - Features overview custom data](./docs/assets/custom-data.jpg "Snapshot - Features overview custom data") 228 | 229 | ```js 230 | customData: { 231 | title: 'Run info', 232 | data: [ 233 | {label: 'Project', value: 'Custom project'}, 234 | {label: 'Release', value: '1.2.3'}, 235 | {label: 'Cycle', value: 'B11221.34321'}, 236 | {label: 'Execution Start Time', value: 'Nov 19th 2017, 02:31 PM EST'}, 237 | {label: 'Execution End Time', value: 'Nov 19th 2017, 02:56 PM EST'} 238 | ] 239 | } 240 | ``` 241 | 242 | #### `customData.title` 243 | - **Type:** `string` 244 | - **Mandatory:** No 245 | - **Default:** `Custom data title` 246 | 247 | Select a title for the custom data block. If not provided it will be defaulted. 248 | 249 | #### `customData.data` 250 | - **Type:** `array` 251 | - **Mandatory:** yes 252 | 253 | The data you want to add. This needs to be in the format 254 | 255 | ```js 256 | data: [ 257 | {label: 'your label', value: 'the represented value'} 258 | ] 259 | ``` 260 | 261 | ## Metadata 262 | The report can also show on which browser / device a feature has been executed. It is shown on the features overview in the table, like this 263 | ![Snapshot - Features overview](./docs/assets/features-overview.jpg) 264 | as well as on the feature overview in the container, like this 265 | ![Snapshot - Features overview](./docs/assets/feature-overview.jpg) 266 | 267 | You can add this by adding the following object to your `capabilities`; 268 | 269 | ```js 270 | exports.config = { 271 | //.. 272 | capabilities: [ 273 | { 274 | browserName: 'chrome', 275 | // Add this 276 | metadata: { 277 | browser: { 278 | name: 'chrome', 279 | version: '58' 280 | }, 281 | device: 'MacBook Pro 15', 282 | platform: { 283 | name: 'OSX', 284 | version: '10.12.6' 285 | } 286 | }, 287 | }, 288 | ], 289 | }; 290 | ``` 291 | 292 | See the metadata information [here](https://github.com/wswebcreation/multiple-cucumber-html-reporter#metadatabrowsername) for the correct values. 293 | 294 | > If you don't provide the `browser`-object in the metadata, this module will automatically determine it for you 295 | 296 | > If you don't provide the `device` and or the `platform`-object it will be defaulted for you to `not known` 297 | 298 | > If you don't provide a `browser.name` or a `browser.version` the module will try to determine this automatically. The rest will be shown as questionmarks in the report 299 | 300 | ## FAQ's 301 | 302 | ### How do I add screenshots to the report 303 | Just create a `After`-hook in a stepfile like this 304 | 305 | ```js 306 | const {After, Status} = require('cucumber'); 307 | import multipleCucumberHtmlReporter from 'wdio-multiple-cucumber-html-reporter'; 308 | 309 | After((scenarioResult)=>{ 310 | if (scenarioResult.result.status === Status.FAILED) { 311 | // It will add the screenshot to the JSON 312 | multipleCucumberHtmlReporter.attach(browser.saveScreenshot(), 'image/png', 'after'); 313 | // Or with 314 | multipleCucumberHtmlReporter.attach(browser.screenshot(), 'image/png', 'after'); 315 | } 316 | return scenarioResult.status; 317 | }); 318 | ``` 319 | 320 | ### I don't see the keywords `Given, When, Then` in the report 321 | The module `wdio-cucumber-framework` only provides this information from version `2.2.0` and higher. Please upgrade to that version. 322 | 323 | ### `skipped` steps are marked as `pending` 324 | `skipped` steps are currently marked as `pending` because `wdio-cucumber-framework` can't distinguish them, there is a PR for this, see [here](https://github.com/webdriverio/wdio-cucumber-framework/pull/34) 325 | 326 | ### `ambiguous` steps are marked as `pending` 327 | CucumberJS has a status called `ambiguous`, this should also be shown in the report. 328 | Because `wdio-cucumber-framework` has it's own implementation to handle ambiguous steps , see [here](https://github.com/webdriverio/wdio-cucumber-framework#failambiguousdefinitions) 329 | it will not show the correct status in the report using this module. 330 | 331 | ### `undefined` steps are not marked as `undefined` 332 | CucumberJS has a status called `undefined`, this should also be shown in the report. 333 | Because `wdio-cucumber-framework` has it's own implementation to handle undefined steps , see [here](https://github.com/webdriverio/wdio-cucumber-framework#ignoreundefineddefinitions) 334 | it will not show the correct status in the report using this module. 335 | 336 | ## Changelog 337 | A changelog can be found [here](./CHANGELOG.md). 338 | 339 | ## Contributing 340 | How to contribute can be found [here](./docs/CONTRIBUTING.md). 341 | 342 | ## TODO: 343 | ### high priority 344 | Needs to be in the first beta 345 | - [x] Add `Before`-step to the json, see the remarks about the Before steps 346 | - [x] Add `After`-step to the json, see the remarks about the After steps 347 | - [x] Add browser data to the report, first start with the default capabilities 348 | - [x] Add screenshots to the report 349 | - [x] Add multiple screenshots to the report in 1 step 350 | - [x] Test in multiple browsers in parallel 351 | - [x] Check / add `Passed` status 352 | - [x] Check / add `Failed` status 353 | - [x] Check / add `Pending` status 354 | - [x] Check / add `Ambiguous` status 355 | - [x] Check / add `Skipped` status 356 | - [x] Check / add `undefined` status 357 | 358 | ### low priority 359 | Needs to be in, but are not mandatory 360 | - [ ] Investigate `Hooks` and if they can influence the outcome 361 | - [ ] Write UT's 362 | - [x] Test on Windows 363 | - [x] Test on Android 364 | - [x] Test on iOS 365 | 366 | ### research 367 | - [x] Find out where the keywords are, there is no `Given, When, Then` comming back from webdriver.io => => Created a PR for [wdio-cucumber-framework 136](https://github.com/webdriverio/wdio-cucumber-framework/pull/136) that has been merged and released as version `2.2.0` 368 | - [x] Add data tables to the report, see [data tables](https://github.com/cucumber/cucumber-js/blob/master/features/data_tables.feature) => Created a PR for [wdio-cucumber-framework 136](https://github.com/webdriverio/wdio-cucumber-framework/pull/136) that has been merged and released as version `2.2.0` 369 | - [ ] Find out where the description of the feature is. Can't find it in the methods and it will not be added to the report then 370 | 371 | ## Some remarks 372 | ### Before hooks 373 | Before hooks are not added to the WebdriverIO reporter. There is no way in telling they passed / failed. 374 | #### Pass 375 | Not logged in `wdio-cucumberjs-framework` => not in this module 376 | #### Failed 377 | Automatically logged by my implementation, not all data is logged like screenshots and so on 378 | 379 | #### Pending 380 | Pending state will result in the following: 381 | - Beforehook will not get the status, there is only a start, not a pass/failed/pending 382 | - All Scenario steps will get status pending 383 | Meaning I can't log this 384 | 385 | ### After hooks 386 | After hooks are not added to the WebdriverIO reporter. There is no way in telling they passed / failed 387 | #### Pass 388 | Not logged in `wdio-cucumberjs-framework` => not in this module 389 | #### Failed 390 | Automatically logged by my implementation, not all data is logged like screenshots and so on 391 | 392 | #### Pending 393 | Status pending of the After hook has no effect on the status of the report / wdio-cucumber-framework will not report this status 394 | -------------------------------------------------------------------------------- /__tests__/app_features/app.feature: -------------------------------------------------------------------------------- 1 | @feature-tag 2 | Feature: Open the WDIO app 3 | 4 | Scenario: Open website 5 | Given I open the app 6 | Then the home screen is shown 7 | -------------------------------------------------------------------------------- /__tests__/config/wdio.app.android.conf.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { config } = require('./wdio.shared.conf'); 3 | 4 | // ============ 5 | // Capabilities 6 | // ============ 7 | // For all capabilities please check 8 | // http://appium.io/docs/en/writing-running-appium/caps/#general-capabilities 9 | config.capabilities = [ 10 | { 11 | // The defaults you need to have in your config 12 | automationName: 'UiAutomator2', 13 | deviceName: 'Pixel_8.1', 14 | platformName: 'Android', 15 | platformVersion: '8.1', 16 | orientation: 'PORTRAIT', 17 | maxInstances: 1, 18 | app: join(process.cwd(), './apps/Android-NativeDemoApp-0.2.1.apk'), 19 | // Read the reset strategies very well, they differ per platform, see 20 | // http://appium.io/docs/en/writing-running-appium/other/reset-strategies/ 21 | noReset: true, 22 | newCommandTimeout: 240, 23 | }, 24 | ]; 25 | 26 | // ==================== 27 | // Specs 28 | // ==================== 29 | config.specs = [ `${ process.cwd() }/__tests__/app_features/app.feature` ]; 30 | 31 | // ==================== 32 | // Appium Configuration 33 | // ==================== 34 | // Default port for Appium 35 | config.port = 4723; 36 | 37 | exports.config = config; 38 | -------------------------------------------------------------------------------- /__tests__/config/wdio.app.ios.conf.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { config } = require('./wdio.shared.conf'); 3 | 4 | // ============ 5 | // Capabilities 6 | // ============ 7 | // For all capabilities please check 8 | // http://appium.io/docs/en/writing-running-appium/caps/#general-capabilities 9 | config.capabilities = [ 10 | { 11 | // The defaults you need to have in your config 12 | deviceName: 'iPhone X', 13 | platformName: 'iOS', 14 | platformVersion: '12.2', 15 | orientation: 'PORTRAIT', 16 | maxInstances: 1, 17 | // The path to the app 18 | app: join(process.cwd(), './apps/iOS-Simulator-NativeDemoApp-0.2.1.app.zip'), 19 | // Read the reset strategies very well, they differ per platform, see 20 | // http://appium.io/docs/en/writing-running-appium/other/reset-strategies/ 21 | noReset: true, 22 | newCommandTimeout: 240, 23 | }, 24 | ]; 25 | 26 | // ==================== 27 | // Specs 28 | // ==================== 29 | config.specs = [ `${ process.cwd() }/__tests__/app_features/app.feature` ]; 30 | 31 | // ==================== 32 | // Appium Configuration 33 | // ==================== 34 | // Default port for Appium 35 | config.port = 4723; 36 | 37 | exports.config = config; 38 | -------------------------------------------------------------------------------- /__tests__/config/wdio.browsers.conf.js: -------------------------------------------------------------------------------- 1 | const { config } = require('./wdio.shared.conf') 2 | 3 | config.capabilities = [ 4 | { 5 | maxInstances: 5, 6 | browserName: 'chrome', 7 | chromeOptions: { 8 | args: [ '--headless', 'disable-infobars' ], 9 | prefs: { 10 | download: { 11 | prompt_for_download: false, 12 | directory_upgrade: true, 13 | default_directory: './tmp', 14 | }, 15 | }, 16 | }, 17 | metadata: { 18 | browser: { 19 | name: 'chrome', 20 | version: '58' 21 | }, 22 | device: 'MacBook Pro 15', 23 | platform: { 24 | name: 'OSX', 25 | version: '10.12.6' 26 | } 27 | }, 28 | }, 29 | // { 30 | // maxInstances: 5, 31 | // browserName: 'firefox', 32 | // 'moz:firefoxOptions': { 33 | // args: ['-headless'] 34 | // } 35 | // }, 36 | ]; 37 | 38 | exports.config = config; 39 | -------------------------------------------------------------------------------- /__tests__/config/wdio.shared.conf.js: -------------------------------------------------------------------------------- 1 | const { argv } = require('yargs'); 2 | const chai = require('chai'); 3 | const multipleCucumberHtmlReporter = require('../../build/reporter'); 4 | multipleCucumberHtmlReporter.reporterName = 'multiple-cucumber-html-reporter'; 5 | 6 | exports.config = { 7 | // ================== 8 | // Test Configuration 9 | // ================== 10 | sync: true, 11 | logLevel: argv.logLevel || 'silent', 12 | coloredLogs: true, 13 | bail: 0, 14 | baseUrl: 'http://webdriver.io', 15 | waitforTimeout: 20000, 16 | connectionRetryTimeout: 90000, 17 | connectionRetryCount: 3, 18 | 19 | // ====================== 20 | // Cucumber configuration 21 | // ====================== 22 | framework: 'cucumber', 23 | specs: getFeatureFiles(), 24 | cucumberOpts: { 25 | require: [ 26 | '__tests__/**/*.steps.js', 27 | ], 28 | backtrace: false, 29 | compiler: [ 'js:babel-register' ], 30 | colors: true, 31 | snippets: true, 32 | source: true, 33 | tagExpression: 'not @wip and not @ignore', 34 | timeout: 60000, 35 | failAmbiguousDefinitions: false, 36 | ignoreUndefinedDefinitions: false, 37 | }, 38 | 39 | // ==================== 40 | // Reporter 41 | // ==================== 42 | reporters: [ 'spec', multipleCucumberHtmlReporter ], 43 | reporterOptions: { 44 | htmlReporter: { 45 | removeFolders: true, 46 | jsonFolder: '.tmp/new/', 47 | reportFolder: '.tmp/multiple-cucumber-html-reporter/', 48 | displayDuration: true, 49 | openReportInBrowser: true, 50 | saveCollectedJSON: true, 51 | disableLog: true, 52 | pageTitle: 'pageTitle', 53 | reportName: 'reportName', 54 | pageFooter: '

Custom footer

', 55 | customData: { 56 | title: 'Run info', 57 | data: [ 58 | { label: 'Project', value: 'Custom project' }, 59 | { label: 'Release', value: '1.2.3' }, 60 | { label: 'Cycle', value: 'B11221.34321' }, 61 | { label: 'Execution Start Time', value: 'Nov 19th 2017, 02:31 PM EST' }, 62 | { label: 'Execution End Time', value: 'Nov 19th 2017, 02:56 PM EST' } 63 | ] 64 | }, 65 | } 66 | }, 67 | 68 | /** 69 | * Gets executed before test execution begins. At this point you can access to all global 70 | * variables like `browser`. 71 | */ 72 | before: () => { 73 | /** 74 | * Setup the Chai assertion framework 75 | */ 76 | global.expect = chai.expect; 77 | global.assert = chai.assert; 78 | global.should = chai.should(); 79 | }, 80 | }; 81 | 82 | /** 83 | * Get the featurefiles that need to be run based on an command line flag that is passed, 84 | * if nothing is passed all the featurefiles are run 85 | * 86 | * @example: 87 | * 88 | *
 89 |  *     // For 1 feature
 90 |  *     npm run test -- --feature=playground
 91 |  *
 92 |  *     // For multiple features
 93 |  *     npm run test -- --feature=playground,login,...
 94 |  *
 95 |  *     // Else
 96 |  *     npm run test
 97 |  * 
98 | */ 99 | function getFeatureFiles() { 100 | if (argv.feature) { 101 | return argv.feature.split(',').map(feature => `${ process.cwd() }/__tests__/**/${ feature }.feature`); 102 | } 103 | 104 | return [ `${ process.cwd() }/__tests__/**/*.feature` ]; 105 | } 106 | -------------------------------------------------------------------------------- /__tests__/features/ambiguous.feature: -------------------------------------------------------------------------------- 1 | Feature: Create an ambiguous feature 2 | 3 | Scenario: an ambiguous scenario 4 | Given an ambiguous step 5 | -------------------------------------------------------------------------------- /__tests__/features/attach.feature: -------------------------------------------------------------------------------- 1 | #@wip 2 | Feature: Example 3 | 4 | Scenario: example attatch 5 | Given I open the url "http://127.0.0.1:8080/" 6 | When I'm a scenario when step 7 | Then I'm a scenario then step 8 | -------------------------------------------------------------------------------- /__tests__/features/data.feature: -------------------------------------------------------------------------------- 1 | #@wip 2 | Feature: Create data feature 3 | This is a feature description 4 | Second description 5 | 6 | # Background: This is a background step 7 | ## This is a background description 8 | # Given I'm a given background 9 | ## When I'm a when background 10 | ## Then I would be a when background 11 | 12 | Scenario: a table scenario 13 | This should be a scenario description 14 | Given a table step 15 | | Cucumber | Cucumis sativus | 16 | | Burr Gherkin | Cucumis anguria | 17 | When I'm a scenario when step 18 | Then I'm a scenario then step 19 | -------------------------------------------------------------------------------- /__tests__/features/failed.feature: -------------------------------------------------------------------------------- 1 | @feature-tag 2 | Feature: Create failed feature 3 | 4 | @scenario-tag 5 | Scenario: Open website 6 | Given I open "http://webdriver.io/" 7 | Then the title would say "WebdriverIO" 8 | -------------------------------------------------------------------------------- /__tests__/features/issue-16.feature: -------------------------------------------------------------------------------- 1 | Feature: Create ignored undefined feature bassed on issue 16 2 | 3 | @ignore 4 | Scenario: I ignore this undefined scenario 5 | Given I ignore what I want to ignore 6 | Then this should not complain or break with an ignored error 7 | -------------------------------------------------------------------------------- /__tests__/features/no.scenario.feature: -------------------------------------------------------------------------------- 1 | Feature: This feature has no scenario steps 2 | -------------------------------------------------------------------------------- /__tests__/features/outline.feature: -------------------------------------------------------------------------------- 1 | Feature: Create scenario outline feature 2 | This is a feature description 3 | Second description 4 | 5 | Scenario Outline: a scenario outline header1: and header2: 6 | Given an outline step 7 | And an outline step 8 | 9 | Examples: 10 | | header1 | header2 | 11 | | outline 1 | value1 | 12 | | outline 2 | value2 | 13 | -------------------------------------------------------------------------------- /__tests__/features/passed.feature: -------------------------------------------------------------------------------- 1 | @feature-tag 2 | Feature: Create passed feature 3 | 4 | @scenario-tag 5 | Scenario: Open website 6 | Given I open "http://webdriver.io/" 7 | Then the title would say "WebdriverIO · Next-gen WebDriver test framework for Node.js" 8 | 9 | @scenario-tag 10 | Scenario: Open other website 11 | Given I open "https://developer.mozilla.org/nl/" 12 | Then the title would say "MDN-webdocumenten" 13 | -------------------------------------------------------------------------------- /__tests__/features/pending.feature: -------------------------------------------------------------------------------- 1 | Feature: Create a pending feature 2 | 3 | Scenario: a pending scenario 4 | Given a pending step 5 | -------------------------------------------------------------------------------- /__tests__/features/skipped.feature: -------------------------------------------------------------------------------- 1 | Feature: Create a skipped feature 2 | 3 | Scenario: a skipped scenario 4 | Given a skipped step 5 | -------------------------------------------------------------------------------- /__tests__/features/undefined.feature: -------------------------------------------------------------------------------- 1 | @wip 2 | Feature: Create an undefined feature 3 | 4 | Scenario: an undefined scenario 5 | Given an undefined step 6 | -------------------------------------------------------------------------------- /__tests__/step_definitions/all.steps.js: -------------------------------------------------------------------------------- 1 | import { After, Before, Given, Status, Then, When } from 'cucumber'; 2 | import multipleCucumberHtmlReporter from '../../build/reporter'; 3 | 4 | Given(/I open "(.*)"/, function (url) { 5 | browser.url(url); 6 | /** 7 | * Enable below to test with double screenshots with the old way 8 | */ 9 | // this.attach(browser.saveScreenshot(), 'image/png'); 10 | // this.attach(browser.saveScreenshot(), 'image/png'); 11 | /** 12 | * Enable below to test with double screenshots with the new way 13 | */ 14 | // browser.saveScreenshot(); 15 | // browser.saveScreenshot(); 16 | }); 17 | 18 | Given(/a table step/, table => { 19 | console.log('table = ', table); 20 | }); 21 | 22 | Given(/a (skipped|pending) step/, stepType => { 23 | return Promise.resolve(stepType); 24 | }); 25 | 26 | Given(/an ambiguous step/, stepType => { 27 | return Promise.resolve('ambiguous'); 28 | }); 29 | 30 | Given(/an ambiguous step/, stepType => { 31 | return Promise.resolve('ambiguous'); 32 | }); 33 | 34 | Given(/an outline (.*) step/, outline => { 35 | return Promise.resolve(); 36 | }); 37 | 38 | Then(/the title would say "(.*)"/, title => { 39 | expect(browser.getTitle()).to.equal(title); 40 | }); 41 | 42 | 43 | Before((scenarioResult) => { 44 | // console.log('Before-hook'); 45 | // console.log('beforeData = ', scenarioResult); 46 | // browser.pause(5000) 47 | // expect(true).to.equal(true); 48 | // return Promise.resolve('pending'); 49 | 50 | multipleCucumberHtmlReporter.attach('Before string 1', 'before'); 51 | multipleCucumberHtmlReporter.attach('Before string 2', 'text/plain', 'before'); 52 | }); 53 | 54 | /** 55 | * Hook for the old report hook 56 | */ 57 | // After(function (scenarioResult) { 58 | // console.log('After-hook'); 59 | // if (scenarioResult.status === Status.FAILED) { 60 | // this.attach(browser.saveScreenshot(), 'image/png'); 61 | // } 62 | // 63 | // // console.log('after data = ', scenarioResult); 64 | // // return scenarioResult.status; 65 | // // expect(true).to.equal(true); 66 | // // return 'pending'; 67 | // 68 | // return scenarioResult.status; 69 | // }); 70 | 71 | /** 72 | * Hook for the new 73 | */ 74 | // After(function (scenarioResult) { 75 | // if (scenarioResult.result.status === Status.FAILED) { 76 | // browser.saveScreenshot() 77 | // } 78 | // 79 | // this.attach('{"name": "some JSON"}', 'application/json'); 80 | // this.attach('Some text'); 81 | // 82 | // return scenarioResult.status; 83 | // }); 84 | 85 | After(scenarioResult => { 86 | if (scenarioResult.result.status === Status.FAILED) { 87 | // It will add the screenshot to the JSON 88 | multipleCucumberHtmlReporter.attach('After string 1', 'after'); 89 | multipleCucumberHtmlReporter.attach(browser.screenshot(), 'image/png', 'after'); 90 | multipleCucumberHtmlReporter.attach('After string 2', 'text/plain', 'after'); 91 | } 92 | return scenarioResult.status; 93 | }); 94 | 95 | /** 96 | * Some extra's 97 | */ 98 | Given(/I'm a given background/, () => {}); 99 | When(/I'm a when background/, () => {}); 100 | When(/I'm a scenario when step/, () => {}); 101 | Then(/I would be a when background/, () => {}); 102 | Then(/I'm a scenario then step/, () => {}); 103 | 104 | 105 | /** 106 | * For the attach 107 | */ 108 | Given(/I open the url "(.*)"/, url => { 109 | browser.url(url) 110 | multipleCucumberHtmlReporter.attach('just a string'); 111 | multipleCucumberHtmlReporter.attach({ 'json-string': true }, 'application/json'); 112 | multipleCucumberHtmlReporter.attach(browser.saveScreenshot(), 'image/png'); 113 | multipleCucumberHtmlReporter.attach(browser.screenshot(), 'image/png'); 114 | }); 115 | 116 | /** 117 | * For the app 118 | */ 119 | Given(/I open the app/, () => {}); 120 | Given(/the home screen is shown/, () => { 121 | expect($('~Home').waitForVisible(20000)).to.equal(true); 122 | }); 123 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you have a new feature or want to fix a bug please create a PR. 4 | 5 | > **Make sure you have Chrome and Firefox on your machine to let the tests work** 6 | 7 | ## Development 8 | Open a second terminal and run `npm run watch`, it will automatically watch all the files 9 | 10 | ## Testing 11 | Currently there are no unit tests. There are some integration tests to see if a report can be generated, run them by running `npm run t`. 12 | -------------------------------------------------------------------------------- /docs/assets/before-after.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wswebcreation/wdio-multiple-cucumber-html-reporter/32c9823f5879120084c4c21775e13588ecbdc24e/docs/assets/before-after.jpg -------------------------------------------------------------------------------- /docs/assets/custom-data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wswebcreation/wdio-multiple-cucumber-html-reporter/32c9823f5879120084c4c21775e13588ecbdc24e/docs/assets/custom-data.jpg -------------------------------------------------------------------------------- /docs/assets/feature-overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wswebcreation/wdio-multiple-cucumber-html-reporter/32c9823f5879120084c4c21775e13588ecbdc24e/docs/assets/feature-overview.jpg -------------------------------------------------------------------------------- /docs/assets/features-overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wswebcreation/wdio-multiple-cucumber-html-reporter/32c9823f5879120084c4c21775e13588ecbdc24e/docs/assets/features-overview.jpg -------------------------------------------------------------------------------- /docs/assets/given-datatables.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wswebcreation/wdio-multiple-cucumber-html-reporter/32c9823f5879120084c4c21775e13588ecbdc24e/docs/assets/given-datatables.jpg -------------------------------------------------------------------------------- /docs/assets/report-overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wswebcreation/wdio-multiple-cucumber-html-reporter/32c9823f5879120084c4c21775e13588ecbdc24e/docs/assets/report-overview.jpg -------------------------------------------------------------------------------- /lib/generateJson.js: -------------------------------------------------------------------------------- 1 | import { ensureDirSync, outputJsonSync } from 'fs-extra'; 2 | import { resolve } from 'path'; 3 | 4 | /** 5 | * Generate the JSON file with all the Cucumber data in it 6 | * 7 | * @param {string} folder the location where the JSON file needs to be saved 8 | * @param {object} data the JSON data 9 | */ 10 | export function generateJson(folder, data) { 11 | const jsonFolder = resolve(process.cwd(), folder); 12 | ensureDirSync(jsonFolder); 13 | const jsonFile = resolve(jsonFolder, `s${(new Date).getTime()}.json`); 14 | const json = []; 15 | 16 | Object.entries(data).forEach(([key, val]) => { 17 | Object.entries(val._elements).forEach(([key1, val1]) => { 18 | val.elements.push(val1); 19 | }); 20 | // Delete temporary data 21 | delete val._elements; 22 | delete val._attachment; 23 | json.push(val) 24 | }); 25 | 26 | outputJsonSync(jsonFile, json) 27 | } 28 | 29 | -------------------------------------------------------------------------------- /lib/reporter.js: -------------------------------------------------------------------------------- 1 | import events from 'events'; 2 | import process from 'process'; 3 | import { removeSync } from 'fs-extra'; 4 | import { generateJson } from './generateJson'; 5 | import { generate } from 'multiple-cucumber-html-reporter'; 6 | 7 | const FEATURE = 'Feature'; 8 | const SCENARIO = 'Scenario'; 9 | const NOT_KNOWN = 'not known'; 10 | const AFTER = 'After'; 11 | const BEFORE = 'Before'; 12 | const TEXT_PLAIN = 'text/plain'; 13 | 14 | class MultipleCucumberHtmlReporter extends events.EventEmitter { 15 | constructor(baseReporter, config, options = {}) { 16 | super(); 17 | 18 | if (!options.htmlReporter) { 19 | throw new Error('Options need to be provided.'); 20 | } 21 | 22 | if (!options.htmlReporter.jsonFolder) { 23 | throw new Error('A path which holds the JSON files should be provided.'); 24 | } 25 | 26 | if (!options.htmlReporter.reportFolder) { 27 | throw new Error('An output path for the reports should be defined, no path was provided.'); 28 | } 29 | 30 | this.options = options.htmlReporter; 31 | // this.baseReporter = baseReporter; 32 | this.instanceData = {}; 33 | this.results = {}; 34 | this.scenarioName = null; 35 | 36 | // this.on('hook:start', ::this.hookStart); 37 | // this.on('hook:end', ::this.hookEnd); 38 | 39 | this.on('start', ::this.onStart); 40 | 41 | // Test framework events 42 | this.on('suite:start', ::this.suiteStart); 43 | // this.on('suite:end', ::this.suiteEnd); 44 | this.on('test:start', ::this.testStart); 45 | this.on('test:pass', ::this.testPass); 46 | this.on('test:fail', ::this.testFail); 47 | this.on('test:pending', ::this.testPending); 48 | 49 | // Runner events (webdriver) 50 | // this.on('start', ::this.start); 51 | // this.on('runner:command', ::this.runnerCommand); 52 | this.on('runner:result', ::this.runnerResult); 53 | this.on('end', ::this.onEnd); 54 | 55 | // Multiple Cucumber HTML events 56 | this.on('mchr:attachment', ::this.mchrAttachment) 57 | } 58 | 59 | onStart() { 60 | if (this.options.removeFolders) { 61 | removeSync(this.options.jsonFolder); 62 | removeSync(this.options.reportFolder) 63 | } 64 | } 65 | 66 | // hookStart(payload) {} 67 | // hookEnd(payload) {} 68 | 69 | suiteStart(suite) { 70 | const cid = suite.cid; 71 | if (!this.results[ cid ]) { 72 | this.results[ cid ] = this.getFeatureDataObject(suite); 73 | } 74 | this.scenarioName = suite.title; 75 | } 76 | 77 | // suiteEnd(suite) {} 78 | 79 | testStart(test) { 80 | if (!this.results[ test.cid ]._elements[ test.parent ]) { 81 | this.results[ test.cid ]._elements[ test.parent ] = this.getScenarioDataObject(test); 82 | } 83 | } 84 | 85 | testPass(test) { 86 | this.results[ test.cid ]._elements[ test.parent ].steps.push(this.getStepDataObject(test, 'passed')); 87 | } 88 | 89 | testFail(test) { 90 | this.results[ test.cid ]._elements[ test.parent ].steps.push(this.getStepDataObject(test, 'failed')); 91 | } 92 | 93 | testPending(test) { 94 | this.results[ test.cid ]._elements[ test.parent ].steps.push(this.getStepDataObject(test, 'pending')); 95 | } 96 | 97 | /** 98 | * Runner 99 | */ 100 | // runnerCommand(command) {} 101 | 102 | runnerResult(result) { 103 | // Save browserdata so it can be used later 104 | const cid = result.cid; 105 | if (!this.instanceData[ cid ]) { 106 | this.instanceData[ cid ] = this.determineMetadata(result); 107 | } 108 | } 109 | 110 | onEnd(payload) { 111 | const jsonFolder = this.options.jsonFolder; 112 | 113 | // Generate the jsons 114 | generateJson(jsonFolder, this.results); 115 | 116 | // generate the report 117 | generate({ 118 | // Required 119 | jsonDir: jsonFolder, 120 | reportPath: this.options.reportFolder, 121 | // Optional 122 | ...(this.options.customData ? { customData: this.options.customData } : {}), 123 | ...(this.options.customStyle ? { customStyle: this.options.customStyle } : {}), 124 | disableLog: this.options.disableLog || false, 125 | displayDuration: this.options.displayDuration || false, 126 | durationInMS: this.options.durationInMS || false, 127 | openReportInBrowser: this.options.openReportInBrowser || false, 128 | ...(this.options.overrideStyle ? { overrideStyle: this.options.overrideStyle } : {}), 129 | ...(this.options.pageFooter ? { pageFooter: this.options.pageFooter } : {}), 130 | ...(this.options.pageTitle ? { pageTitle: this.options.pageTitle } : {}), 131 | ...(this.options.reportName ? { reportName: this.options.reportName } : {}), 132 | saveCollectedJSON: this.options.saveCollectedJSON || false, 133 | }); 134 | } 135 | 136 | /** 137 | * All functions 138 | */ 139 | 140 | /** 141 | * Get the feature data object 142 | * 143 | * @param {object} featureData 144 | * 145 | * @returns { 146 | * { 147 | * keyword: string, 148 | * line: number, 149 | * name: string, 150 | * tags: string, 151 | * uri: string, 152 | * _elements: {object}, 153 | * elements: Array, 154 | * id: string, 155 | * _attachment: Array 156 | * } 157 | * } 158 | */ 159 | getFeatureDataObject(featureData) { 160 | return { 161 | keyword: FEATURE, 162 | description: this.escapeHTML(featureData.description), 163 | line: parseInt(featureData.uid.substring(featureData.title.length, featureData.uid.length)), 164 | name: this.escapeHTML(featureData.title), 165 | tags: featureData.tags, 166 | uri: featureData.specs[ 0 ], 167 | _elements: {}, // Temporary. All data will be copied to the `elements` when done 168 | elements: [], 169 | id: featureData.title.replace(/ /g, '-').toLowerCase(), 170 | _attachment: [], // Temporary, attachments will be added here and removed per step... 171 | ...this.instanceData[ featureData.cid ], 172 | } 173 | } 174 | 175 | /** 176 | * Get the scenario data object 177 | * 178 | * @param {object} scenarioData This is the testdata of the current scenario 179 | * 180 | * @returns { 181 | * { 182 | * keyword: string, 183 | * line: number, 184 | * name: string, 185 | * tags: string, 186 | * id: string, 187 | * steps: Array 188 | * } 189 | * } 190 | */ 191 | getScenarioDataObject(scenarioData) { 192 | return { 193 | keyword: SCENARIO, 194 | description: this.escapeHTML(scenarioData.description), 195 | line: parseInt(scenarioData.parent.substring(this.scenarioName.length, scenarioData.parent.length)), 196 | name: this.escapeHTML(this.scenarioName), 197 | tags: scenarioData.tags, 198 | id: `${ this.results[ scenarioData.cid ].id };${ this.scenarioName.replace(/ /g, '-').toLowerCase() }`, 199 | steps: [] 200 | } 201 | } 202 | 203 | /** 204 | * Get the step data object 205 | * 206 | * @param {object} stepData This is the testdata of the step 207 | * @param status 208 | * @returns {{arguments: Array, keyword: string, name: *, result: {status: *, duration: *}, line: number, match: {location: string}}} 209 | */ 210 | getStepDataObject(stepData, status) { 211 | const stepResult = { 212 | arguments: stepData.argument || [], 213 | // keyword: ' ', 214 | keyword: stepData.keyword || ' ', 215 | name: this.escapeHTML(stepData.title), 216 | result: { 217 | status: status, 218 | duration: (stepData.duration * 1000000), 219 | ...this.addFailedMessage(stepData, status) 220 | }, 221 | line: parseInt(stepData.uid.substring(stepData.title.length, stepData.uid.length)), 222 | // Add the screenshot embeddings if there are screenshots 223 | ...this.defineEmbeddings(stepData.cid), 224 | match: { 225 | location: 'can not be determined with webdriver.io' 226 | } 227 | } 228 | ; 229 | 230 | return stepResult; 231 | } 232 | 233 | /** 234 | * Define the embeddings 235 | * 236 | * @param {string} cid The current instance id 237 | * 238 | * @returns {{ 239 | * embeddings: * 240 | * } || {} 241 | * } 242 | */ 243 | defineEmbeddings(cid) { 244 | const embeddings = this.results[ cid ]._attachment.length > 0 245 | ? { embeddings: [ ...this.results[ cid ]._attachment ] } 246 | : {}; 247 | 248 | // Empty the attachments because there is no data anymore, step has finished 249 | this.results[ cid ]._attachment = []; 250 | 251 | return embeddings; 252 | } 253 | 254 | /** 255 | * Add a failed message 256 | * 257 | * @param {object} testObject 258 | * @param {string} status 259 | * 260 | * @return {object} 261 | */ 262 | addFailedMessage(testObject, status) { 263 | if (status === 'failed') { 264 | return { 265 | error_message: testObject.err.stack 266 | } 267 | } 268 | 269 | return {} 270 | } 271 | 272 | /** 273 | * Determine the metadata that needs to be added 274 | * 275 | * @TODO: Need to look at the implementation, is not that nice 276 | * 277 | * @param {object} data instance data 278 | * 279 | * @returns { 280 | * { 281 | * metadata: { 282 | * app: { 283 | * name: string, 284 | * version: string 285 | * }, 286 | * browser: { 287 | * name: string, 288 | * version: string 289 | * }, 290 | * device: string, 291 | * platform: { 292 | * name: string, 293 | * version: string 294 | * } 295 | * } 296 | * } 297 | * } 298 | */ 299 | determineMetadata(data) { 300 | let app, browser; 301 | const desiredCapabilities = data.requestData.desiredCapabilities; 302 | const metadata = desiredCapabilities.metadata || {}; 303 | const bodyValue = data.body.value || {}; 304 | 305 | // When an app is used to test 306 | if (bodyValue.app || bodyValue.testobject_app_id || metadata.app) { 307 | const metaAppName = (metadata.app && metadata.app.name) ? metadata.app.name : 'No metadata.app.name available'; 308 | const metaAppVersion = (metadata.app && metadata.app.version) ? metadata.app.version : 'No metadata.app.version available'; 309 | const appPath = (bodyValue.app || bodyValue.testobject_app_id || metaAppName); 310 | const appName = appPath.substring(appPath.replace('\\', '/').lastIndexOf('/')).replace('/', ''); 311 | 312 | // Provide the app name and version 313 | app = { 314 | app: { 315 | name: appName, 316 | version: metaAppVersion 317 | } 318 | }; 319 | // Provide the devicename if there 320 | metadata.device = desiredCapabilities.deviceName 321 | || bodyValue.deviceName 322 | || metadata.device 323 | || null; 324 | metadata.platform = { 325 | name: desiredCapabilities.platformName 326 | || bodyValue.platformName 327 | || (metadata.platform && metadata.platform.name) 328 | || 'No metadata.platform.name available', 329 | version: desiredCapabilities.platformVersion 330 | || bodyValue.platformVersion 331 | || (metadata.platform && metadata.platform.version) 332 | || 'No metadata.platform.version available', 333 | } 334 | } else { 335 | const browserName = bodyValue.browserName 336 | || ((metadata && metadata.browser && metadata.browser.name) ? metadata.browser.name : 'No metadata.browser.name available') 337 | const browserVersion = bodyValue.version 338 | || bodyValue.browserVersion 339 | || ((metadata && metadata.browser && metadata.browser.version) ? metadata.browser.version : 'No metadata.browser.version available') 340 | 341 | browser = { 342 | browser: { 343 | name: browserName, 344 | version: browserVersion, 345 | } 346 | }; 347 | } 348 | 349 | const device = (metadata && metadata.device) ? metadata.device : NOT_KNOWN; 350 | const platform = { 351 | name: (metadata && metadata.platform && metadata.platform.name) ? metadata.platform.name : NOT_KNOWN, 352 | version: (metadata && metadata.platform && metadata.platform.version) ? metadata.platform.version : NOT_KNOWN 353 | }; 354 | 355 | return { 356 | metadata: { 357 | ...(app || browser), 358 | device, 359 | platform, 360 | } 361 | } 362 | } 363 | 364 | /** 365 | * Escape html in strings 366 | * 367 | * @param {string} string 368 | * @return {string} 369 | */ 370 | escapeHTML(string) { 371 | return !string ? string : string.replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"').replace(/\'/g, '''); 372 | } 373 | 374 | /** 375 | * Attach data to the report 376 | * 377 | * @param {string|object} data 378 | * @param {string} type Default is `text/plain`, otherwise what people deliver as a MIME type, like `application/json`, `image/png` 379 | * @param {string|undefined} hookType This will tell if the attach needs to take place for a before or after hook 380 | */ 381 | static attach(data, type = TEXT_PLAIN, hookType = undefined) { 382 | process.send({ event: 'mchr:attachment', ...{ data, type, hookType } }) 383 | } 384 | 385 | /** 386 | * Add the attachment to the result 387 | * 388 | * @param {string} cid 389 | * @param {string|object} data 390 | * @param {string} type 391 | * @param {string|undefined} hookType This will tell if the attach needs to take place for a before or after hook 392 | */ 393 | mchrAttachment({ cid, data, type, hookType }) { 394 | let hook; 395 | // It could be that people don't provide the type, but just the hookType, so check that here 396 | type = this.validateHookType(type); 397 | if (type === BEFORE || type === AFTER) { 398 | hook = type; 399 | // Set the type to the default if the type has been used to set the hookType 400 | type = TEXT_PLAIN; 401 | } 402 | 403 | if (hookType) { 404 | hook = this.validateHookType(hookType); 405 | } 406 | 407 | if (data.value) { 408 | data = data.value; 409 | } else if (data.data) { 410 | data = Buffer.from(data.data).toString('base64'); 411 | } 412 | 413 | // This will push all data that has been attached to the global, this will be parsed and cleaned for 414 | // each executed step (test) in the `passed||failed||pending`-state 415 | this.results[ cid ]._attachment.push({ 416 | data: data, 417 | mime_type: type, 418 | }); 419 | 420 | // If a hook is used, add the hook data 421 | if (hook) { 422 | this.addHookData(cid, hook); 423 | } 424 | } 425 | 426 | /** 427 | * Validate if the hook type that is used is a valid hook or not 428 | * 429 | * @param {string} string 430 | * 431 | * @return {string} 432 | */ 433 | validateHookType(string) { 434 | if (string && (this.capitalizeFirstLetter(string) === AFTER || this.capitalizeFirstLetter(string) === BEFORE)) { 435 | return this.capitalizeFirstLetter(string) === AFTER ? 'After' : 'Before'; 436 | } 437 | 438 | return string; 439 | } 440 | 441 | /** 442 | * Capitalize the first letter of the string 443 | * 444 | * @param {string} string 445 | * 446 | * @return {string} 447 | */ 448 | capitalizeFirstLetter(string) { 449 | return string.charAt(0).toUpperCase() + string.toLowerCase().slice(1); 450 | } 451 | 452 | /** 453 | * Determine the hook data and determine if a before or after hook step needs to be made 454 | * 455 | * @param {number} cid 456 | * @param {string} hookType 457 | */ 458 | addHookData(cid, hookType) { 459 | const scenarios = this.results[ cid ]._elements; 460 | const currentScenario = Object.keys(scenarios)[ Object.keys(scenarios).length - 1 ]; 461 | const hookStepData = { 462 | keyword: hookType, 463 | title: '', 464 | uid: '', 465 | duration: 0, 466 | cid: cid, 467 | } 468 | const stepsArray = scenarios[ currentScenario ].steps; 469 | 470 | if (hookType === AFTER) { 471 | this.addAfterHookData(cid, currentScenario, hookStepData, stepsArray) 472 | } else { 473 | this.addBeforeHookData(cid, currentScenario, hookStepData, stepsArray) 474 | } 475 | } 476 | 477 | /** 478 | * Add after hook data, also determine if there is already an after hook, if so, add it to the current one 479 | * 480 | * @param {number} cid 481 | * @param {string} currentScenario 482 | * @param {string} hookStepData 483 | * @param {object} stepsArray 484 | */ 485 | addAfterHookData(cid, currentScenario, hookStepData, stepsArray) { 486 | if (stepsArray[ stepsArray.length - 1 ].keyword === AFTER) { 487 | stepsArray[ stepsArray.length - 1 ].embeddings.push(this.results[ cid ]._attachment[ 0 ]) 488 | this.results[ cid ]._attachment = []; 489 | } else { 490 | const hookData = this.getStepDataObject(hookStepData, 'passed'); 491 | this.results[ cid ]._elements[ currentScenario ].steps.push(hookData); 492 | } 493 | } 494 | 495 | /** 496 | * Add before hook data, also determine if there is already a before hook, if so, add it to the current one 497 | * 498 | * @param {number} cid 499 | * @param {string} currentScenario 500 | * @param {string} hookStepData 501 | * @param {object} stepsArray 502 | */ 503 | addBeforeHookData(cid, currentScenario, hookStepData, stepsArray) { 504 | if (stepsArray[ 0 ] && stepsArray[ 0 ].keyword === BEFORE) { 505 | stepsArray[ stepsArray.length - 1 ].embeddings.push(this.results[ cid ]._attachment[ 0 ]) 506 | this.results[ cid ]._attachment = []; 507 | } else { 508 | const hookData = this.getStepDataObject(hookStepData, 'passed'); 509 | this.results[ cid ]._elements[ currentScenario ].steps.unshift(hookData); 510 | } 511 | } 512 | } 513 | 514 | MultipleCucumberHtmlReporter.reporterName = 'multiple-cucumber-html-reporter'; 515 | export default MultipleCucumberHtmlReporter; 516 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wdio-multiple-cucumber-html-reporter", 3 | "version": "1.1.1", 4 | "description": "A WebdriverIO plugin. Report results in multiple-cucumber-html-reporter format.", 5 | "main": "build/reporter.js", 6 | "scripts": { 7 | "clean": "rimraf coverage build .tmp", 8 | "compile": "babel lib/ -d build/", 9 | "release": "np", 10 | "precompile": "npm run clean", 11 | "prerelease": "npm run compile", 12 | "prepublish": "npm run compile", 13 | "test.browsers": "node_modules/.bin/wdio __tests__/config/wdio.browsers.conf.js", 14 | "test.app.android": "node_modules/.bin/wdio __tests__/config/wdio.app.android.conf.js", 15 | "test.app.ios": "node_modules/.bin/wdio __tests__/config/wdio.app.ios.conf.js", 16 | "watch": "npm run compile -- --watch" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/wswebcreation/wdio-multiple-cucumber-html-reporter.git" 21 | }, 22 | "keywords": [ 23 | "multiple-cucumber-html-reporter", 24 | "cucumberjs", 25 | "reporter", 26 | "report", 27 | "webdriverio", 28 | "wdio", 29 | "wdio-plugin", 30 | "wdio-reporter" 31 | ], 32 | "author": "Wim Selles ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/wswebcreation/wdio-multiple-cucumber-html-reporter/issues" 36 | }, 37 | "homepage": "https://github.com/wswebcreation/wdio-multiple-cucumber-html-reporter#readme", 38 | "dependencies": { 39 | "babel-runtime": "^6.26.0", 40 | "fs-extra": "^6.0.1", 41 | "multiple-cucumber-html-reporter": "^1.11.6" 42 | }, 43 | "peerDependencies": { 44 | "wdio-cucumber-framework": "^2.2.0" 45 | }, 46 | "devDependencies": { 47 | "babel-cli": "^6.26.0", 48 | "babel-core": "^6.26.3", 49 | "babel-eslint": "^8.2.3", 50 | "babel-plugin-add-module-exports": "^0.2.1", 51 | "babel-plugin-transform-function-bind": "^6.22.0", 52 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 53 | "babel-preset-env": "^1.7.0", 54 | "chai": "^4.2.0", 55 | "eslint": "^4.19.1", 56 | "np": "^5.0.1", 57 | "rimraf": "^2.6.3", 58 | "wdio-cucumber-framework": "^2.2.8", 59 | "wdio-selenium-standalone-service": "0.0.10", 60 | "wdio-spec-reporter": "^0.1.4", 61 | "webdriverio": "^4.14.4", 62 | "yargs": "^11.0.0" 63 | } 64 | } 65 | --------------------------------------------------------------------------------