├── index.js ├── .jshintrc ├── History.md ├── .gitignore ├── package.json ├── LICENSE ├── readme.md └── lib └── VideoReporter.js /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/VideoReporter'); -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "curly": true, 6 | "immed": true, 7 | "newcap": true, 8 | "noarg": true, 9 | "undef": true, 10 | "unused": "vars", 11 | "strict": true 12 | } -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2 4 | 5 | * Breaking: Add the `singleVideo` option and make it the default. If you want the previous behavior a separate file for every spec than you need to set `singleVideo: false`. 6 | * Breaking: remove `debug` option. If you want debugging information you need to set the `DEBUG` environment variable to `protractor-video-reporter` (see the [debug](https://github.com/visionmedia/debug) module). 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "protractor-video-reporter", 3 | "version": "0.3.0", 4 | "description": "A jasmine2 reporter to capture a video screen cast of Protractor specs run with xvfb", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/tepez/protractor-video-reporter.git" 12 | }, 13 | "author": "Tom Tepez Yam (https://github.com/tepez)", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/tepez/protractor-video-reporter/issues" 17 | }, 18 | "homepage": "https://github.com/tepez/protractor-video-reporter", 19 | "dependencies": { 20 | "debug": "^2.2.0", 21 | "joi": "^8.0.4", 22 | "lodash": "^4.6.0", 23 | "mkdirp": "^0.5.1", 24 | "node-uuid": "^1.4.3", 25 | "subtitles-parser": "0.0.2", 26 | "sanitize-filename": "^1.6.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tom Yam 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 | 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # protractor-video-reporter 2 | 3 | A Jasmine 2 reporter that captures a screencast of Protractor specs running on a headless browser, e.g. under Xvfb. 4 | This is especially usefull for debugging you e2e specs on you CI server. 5 | The reporter also creates a SRT subtitles file to for the video so you can see which spec you are currently viewing and whether it passed or failed. 6 | 7 | # Install 8 | 9 | npm install --save-dev protractor-video-reporter 10 | 11 | # Prerequisites 12 | 13 | You have to start Xvfb before starting Protractor and set the `DISPLAY` enviroment variable. 14 | 15 | If you're using Jenkins CI, you can use the [Xvfb plugin](https://wiki.jenkins-ci.org/display/JENKINS/Xvfb+Plugin) to easily achive that. 16 | 17 | # Usage 18 | 19 | In the protractor configuration file: 20 | 21 | var VideoReporter = require('protractor-video-reporter'); 22 | 23 | ... 24 | 25 | onPrepare: function() { 26 | ... 27 | jasmine.getEnv().addReporter(new VideoReporter({ 28 | baseDirectory: Path.join(__dirname, 'reports/videos/') 29 | })); 30 | } 31 | 32 | 33 | # Options 34 | 35 | * `baseDirectory` (string): The path to the directory where videos are stored. If not existing, it gets created. Required. 36 | 37 | * `singleVideo` (bool): If `true`, will create a single video file for all the specs. Defaults to `true`. 38 | The file will be saved to `baseDirectory/protractor-specs.mov`. 39 | If `singleVideo` is false, the reporter will create a separate video file for every spec and place it under the `baseDirectory`. 40 | The exact location is determined by `singleVideoPath`. 41 | 42 | * `singleVideoPath`: (string, function): 43 | 44 | When `uuid` (default): Each spec video file will be placed at `baseDirectory/{some random UUID}.mov`. 45 | If you prefer this option, you would have to look at the "Spec video is in: ..." messages that are printed to the console. 46 | 47 | When `fullName`: Each spec video will be placed at `baseDirectory/{spec full name} - {spec status}.mov`. 48 | The full name of the spec will be sanitized to be a valid file name 49 | 50 | If you want to determine the full name yourself you can pass a function. 51 | The function recieves a single argument, the result object passed to `specStarted`. 52 | For example, you can do: 53 | 54 | singleVideoPath: function (result) { 55 | // don't actually do this, you need to make sure fullName is a valid file name 56 | result.fullName + '.mov'; 57 | } 58 | 59 | * `createSubtitles` (bool): If `true` and singleVideo is also true, will create a SRT subtitles file with the name details of the currently running spec. Defaults to `true`. 60 | The file will be saves to `baseDirectory/protractor-specs.srt`. 61 | 62 | * `saveSuccessVideos` (bool): If `true`, will save the videos of the succussfull specs, as well as the failed specs. This has no effect if `singleVideo` is `true` - we'll always capture all the specs then. Defaults to `false`. 63 | 64 | * `ffmpegCmd` (string): The command used to execute ffmpeg, e.g. `'/usr/bin/ffmpeg'`. Defaults to `'ffmpeg'`. 65 | 66 | * `ffmpegArgs` (array): The argumetns passed to ffmpeg, not including the actual output file which will be appended. Defaults to: 67 | 68 | ``` 69 | [ 70 | '-y', 71 | '-r', '30', 72 | '-f', 'x11grab', 73 | '-s', '1024x768', 74 | '-i', process.env.DISPLAY, 75 | '-g', '300', 76 | '-vcodec', 'qtrle', 77 | ] 78 | ``` 79 | 80 | # Debugging 81 | 82 | If you encouter any issues with the reporter, e.g. video files are not created, 83 | turn on debugging by settings the `DEBUG` environment to `protractor-video-reporter`. 84 | -------------------------------------------------------------------------------- /lib/VideoReporter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Joi = require('joi'), 4 | Path = require('path'), 5 | ChildProcess = require('child_process'), 6 | Fs = require('fs'), 7 | Mkdirp = require('mkdirp'), 8 | Uuid = require('node-uuid'), 9 | Debug = require('debug'), 10 | SubtitlesParser = require('subtitles-parser'), 11 | _ = require('lodash'), 12 | SanitizeFilename = require('sanitize-filename'); 13 | 14 | 15 | var debug = Debug('protractor-video-reporter'); 16 | 17 | function randomVideoName() { 18 | 19 | } 20 | 21 | function VideoReporter(options) { 22 | 23 | var self = this; 24 | 25 | options = _.defaults({}, options, { 26 | saveSuccessVideos: false, 27 | singleVideo: true, 28 | singleVideoPath: 'uuid', 29 | createSubtitles: true, 30 | ffmpegCmd: 'ffmpeg', 31 | ffmpegArgs: [ 32 | '-y', 33 | '-r', '30', 34 | '-f', 'x11grab', 35 | '-s', '1024x768', 36 | '-i', process.env.DISPLAY, 37 | '-g', '300', 38 | '-vcodec', 'qtrle', 39 | ] 40 | }); 41 | 42 | // validate options 43 | var result = Joi.validate(options, Joi.object().keys({ 44 | baseDirectory: Joi.string() 45 | .description('The path to the directory where videos are stored. If not existing, it gets created.'), 46 | saveSuccessVideos: Joi.boolean() 47 | .description('If true, will save the videos of the succussfull specs, as well as the failed specs.'), 48 | singleVideo: Joi.boolean() 49 | .description('If true, will create a single video file for all the specs.'), 50 | singleVideoPath: Joi.alternatives().try( 51 | Joi.valid('uuid', 'fullName'), 52 | Joi.func() 53 | ), 54 | createSubtitles: Joi.boolean() 55 | .description('If true and singleVideo is also true, will create a SRT subtitles file with the name details of the currently running spec.'), 56 | 57 | ffmpegCmd: Joi.string() 58 | .description('The command used to execute ffmpeg, e.g. /usr/bin/ffmpeg.'), 59 | ffmpegArgs: Joi.array().items(Joi.string(), Joi.number()) 60 | .description('The argumetns passed to ffmpeg, not including the actual output file which will be appended.') 61 | })); 62 | 63 | if (result.error) { 64 | throw result.error; 65 | } 66 | 67 | self.options = result.value; 68 | } 69 | 70 | VideoReporter.prototype._startScreencast = function(videoPath) { 71 | 72 | var self = this; 73 | 74 | self._videoPath = videoPath; 75 | debug('Saving video to ' + self._videoPath); 76 | 77 | // Make sure that directory exists 78 | Mkdirp.sync(Path.dirname(self._videoPath)); 79 | 80 | var ffmpegArgs = _.clone(self.options.ffmpegArgs); 81 | ffmpegArgs.push(self._videoPath); 82 | 83 | debug('Spawning: ' + self.options.ffmpegCmd + ' ' + ffmpegArgs.join(' ')); 84 | self._ffmpeg = ChildProcess.spawn(self.options.ffmpegCmd, ffmpegArgs); 85 | 86 | self._ffmpeg.stdout.on('data', function (data) { 87 | debug('ffmpeg (out): ' + data); 88 | }); 89 | 90 | self._ffmpeg.stderr.on('data', function (data) { 91 | debug('ffmpeg (err): ' + data); 92 | }); 93 | 94 | self._ffmpeg.on('close', function (code) { 95 | debug('ffmpeg exited with code ' + code); 96 | }); 97 | 98 | }; 99 | 100 | 101 | VideoReporter.prototype._stopScreencast = function(removeVideo) { 102 | var self = this; 103 | 104 | // Stop ffmpeg 105 | self._ffmpeg.kill(); 106 | 107 | switch (removeVideo) { 108 | case 'yes': 109 | debug('Removing video'); 110 | Fs.unlinkSync(self._videoPath); 111 | break; 112 | 113 | case 'try': 114 | debug('Trying to remove the video, maybe it was not created'); 115 | if (Fs.existsSync(self._videoPath)) { 116 | Fs.unlinkSync(self._videoPath); 117 | } 118 | break; 119 | 120 | case 'no': 121 | debug('Keeping the video'); 122 | console.log('Spec video is in: ' + self._videoPath); 123 | break; 124 | } 125 | 126 | // Cleanup before next spec 127 | self._videoPath = null; 128 | self._ffmpeg = null; 129 | }; 130 | 131 | VideoReporter.prototype._singleVideoPath = function(result) { 132 | var self = this; 133 | if (self.options.singleVideoPath === 'uuid') { 134 | return Uuid.v4() + '.mov'; 135 | } else if (self.options.singleVideoPath === 'fullName') { 136 | return SanitizeFilename(result.fullName + '.mov'); 137 | } else { 138 | return self.options.singleVideoPath(result); 139 | } 140 | } 141 | 142 | 143 | VideoReporter.prototype.specStarted = function(result) { 144 | var self = this; 145 | if (!self.options.singleVideo) { 146 | var videoPath = Path.join(self.options.baseDirectory, self._singleVideoPath(result)); 147 | self._startScreencast(videoPath); 148 | 149 | } else if (self.options.createSubtitles) { 150 | self._currentSubtitle = { 151 | id: self._subtitles.length + 1, 152 | startTime: new Date() - self._jasmineStartTime 153 | }; 154 | } 155 | }; 156 | 157 | VideoReporter.prototype.specDone = function(result) { 158 | var self = this; 159 | if (!self.options.singleVideo) { 160 | 161 | var removeVideo; 162 | 163 | if (result.status === 'failed' || (result.status === 'passed' && self.options.saveSuccessVideos)) { 164 | removeVideo = 'no'; 165 | 166 | // If the spec was not run then the file probably was not created yet 167 | // so we just try to delete it 168 | } else if (result.status === 'pending' || result.status === 'disabled') { 169 | removeVideo = 'try'; 170 | } else { 171 | removeVideo = 'yes'; 172 | } 173 | 174 | self._stopScreencast(removeVideo); 175 | 176 | } else if (self.options.createSubtitles) { 177 | self._currentSubtitle.endTime = new Date() - self._jasmineStartTime; 178 | 179 | var text = ''; 180 | switch (result.status) { 181 | case 'passed': 182 | text += 'SUCCESS'; 183 | break; 184 | case 'failed': 185 | text += 'FAILED'; 186 | break; 187 | case 'pending': 188 | text += 'PENDING'; 189 | break; 190 | } 191 | text += ' ' + result.description; 192 | self._currentSubtitle.text = text; 193 | 194 | self._subtitles.push(self._currentSubtitle); 195 | } 196 | }; 197 | 198 | VideoReporter.prototype.jasmineStarted = function() { 199 | var self = this; 200 | if (self.options.singleVideo) { 201 | var videoPath = Path.join(self.options.baseDirectory, 'protractor-specs.mov'); 202 | self._startScreencast(videoPath); 203 | 204 | if (self.options.createSubtitles) { 205 | self._subtitles = []; 206 | self._jasmineStartTime = new Date(); 207 | } 208 | } 209 | }; 210 | 211 | VideoReporter.prototype.jasmineDone = function() { 212 | var self = this; 213 | if (self.options.singleVideo) { 214 | self._stopScreencast('no'); 215 | 216 | if (self.options.createSubtitles) { 217 | Fs.writeFileSync( 218 | Path.join(self.options.baseDirectory, 'protractor-specs.srt'), 219 | SubtitlesParser.toSrt(self._subtitles), 220 | 'utf8' 221 | ); 222 | } 223 | } 224 | }; 225 | 226 | module.exports = VideoReporter; --------------------------------------------------------------------------------