diff --git a/README.md b/README.md index 055d0cd4..724468f8 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ npm install -g oxygen-cli #### Windows: * ```npm --add-python-to-path='true' --debug install --global windows-build-tools``` from ```cmd``` with admin rights. * [Optional. Required for DB support] Windows SDK +* [Optional. Required for Video recording support] https://github.com/BtbN/FFmpeg-Builds/releases win64-lgpl-shared-4.4\bin add to PATH env #### Linux * [Optional. Required for DB support] unixodbc binaries and development libraries: diff --git a/src/core/OxygenCore.js b/src/core/OxygenCore.js index 11e1b610..64a4a649 100644 --- a/src/core/OxygenCore.js +++ b/src/core/OxygenCore.js @@ -64,7 +64,8 @@ const DEFAULT_CTX = { const DEFAULT_RESULT_STORE = { steps: [], logs: [], - har: null + har: null, + video: null }; export default class Oxygen extends OxygenEvents { @@ -205,6 +206,7 @@ export default class Oxygen extends OxygenEvents { this.resultStore.steps = []; this.resultStore.logs = []; this.har = null; + this.video = null; } async onBeforeCase(context) { @@ -216,6 +218,7 @@ export default class Oxygen extends OxygenEvents { try { module.onBeforeCase && await module.onBeforeCase(context); module._iterationStart && await module._iterationStart(); + await this._callServicesonBeforeCase(context, module); } catch (e) { this.logger.error(`Failed to call "onBeforeCase" method of ${moduleName} module.`, e); @@ -233,6 +236,7 @@ export default class Oxygen extends OxygenEvents { // await for avoid stuck on *.dispose call module.onAfterCase && await module.onAfterCase(error); module._iterationEnd && await module._iterationEnd(error); + await this._callServicesonAfterCase(module); } catch (e) { this.logger.error(`Failed to call "onAfterCase" method of ${moduleName} module.`, e); @@ -905,6 +909,44 @@ export default class Oxygen extends OxygenEvents { } } } + async _callServicesonBeforeCase(context, module) { + if (!this || !this.services) { + return; + } + for (let serviceName in this.services) { + const service = this.services[serviceName]; + if (!service) { + continue; + } + try { + if (service.onBeforeCase) { + service.onBeforeCase(context, module); + } + } + catch (e) { + this.logger.error(`Failed to call "_callServicesonBeforeCase" method of ${serviceName} service.`, e); + } + } + } + async _callServicesonAfterCase(module) { + if (!this || !this.services) { + return; + } + for (let serviceName in this.services) { + const service = this.services[serviceName]; + if (!service) { + continue; + } + try { + if (service.onAfterCase) { + service.onAfterCase(module); + } + } + catch (e) { + this.logger.error(`Failed to call "_callServicesonAfterCase" method of ${serviceName} service.`, e); + } + } + } _populateParametersValue(args) { if (!args || !Array.isArray(args) || args.length == 0) { return args; diff --git a/src/model/case-result.js b/src/model/case-result.js index 5ec2c40e..82b57133 100644 --- a/src/model/case-result.js +++ b/src/model/case-result.js @@ -22,6 +22,7 @@ module.exports = function () { steps: [], // array of step-result.js logs: [], har: null, + video: null, failure: null, context: null }; diff --git a/src/ox_reporters/html/tests-details.ejs b/src/ox_reporters/html/tests-details.ejs index 32d46e75..4d33b875 100644 --- a/src/ox_reporters/html/tests-details.ejs +++ b/src/ox_reporters/html/tests-details.ejs @@ -52,6 +52,9 @@ <%= caseResult.name %> (Iteration #<%= caseResult.iterationNum %>) <%- caseResult.status === 'passed' ? PASSED_LABEL : FAILED_LABEL %> + <% if (caseResult.video) { %> + Video + <% } %>
diff --git a/src/ox_services/service-video.js b/src/ox_services/service-video.js new file mode 100644 index 00000000..4240178f --- /dev/null +++ b/src/ox_services/service-video.js @@ -0,0 +1,81 @@ +import OxygenService from '../core/OxygenService'; +import path from 'path'; + +const allowedModules = ['web']; + +export default class VideoService extends OxygenService { + constructor(options, ctx, results, logger) { + super(options, ctx, results, logger); + this.enabled = options.enableVideo || false; + this.videoFolderPath = options.videoFolderPath || null; + this.results = results; + this.logger = logger; + } + logBuffer (buffer, prefix) { + const lines = buffer.toString().trim().split('\n'); + lines.forEach((line) => { + this.logger.debug(prefix + line); + }); + } + onBeforeCase(context, module) { + if (this.enabled && allowedModules.includes(module.name)) { + let name = +new Date(); + if ( + context && + context.test && + context.test.suite && + context.test.case + ) { + name = context.test.suite.name + '_' + context.test.suite.iteration + '_' + context.test.case.name + '_' + context.test.case.iteration; + } + + this.videoPath = path.join(this.videoFolderPath, name+'.mp4'); + const { spawn } = require('child_process'); + try { + this.ffmpeg = spawn('ffmpeg', [ + '-f', + 'gdigrab', + '-framerate', + 10, + '-i', + 'desktop', + this.videoPath, // Output file + '-y', // Overwrite output files without asking + '-loglevel', + 'error', // Log only errors + ]); + this.ffmpeg.on('error', (e) => { + this.logger.error('Failed to init ffmpeg' + e); + }); + } catch (e) { + this.logger.error('Failed to init ffmpeg' + e); + } + + this.ffmpeg.stdout.on('data', (data) => { + this.logBuffer(data, 'ffmpeg stdout: '); + }); + + this.ffmpeg.stderr.on('data', (data) => { + this.logBuffer(data, 'ffmpeg stderr: '); + }); + + this.ffmpeg.on('close', (code, signal) => { + this.logger.debug('Video location:', this.videoPath, '\n'); + if (code !== null) { + this.logger.debug(`ffmpeg exited with code ${code} ${this.videoPath}`); + } + if (signal !== null) { + this.logger.debug(`ffmpeg received signal ${signal} ${this.videoPath}`); + } + }); + } + } + onAfterCase(module) { + if (this.enabled && allowedModules.includes(module.name)) { + if (this.ffmpeg) { + this.ffmpeg.kill('SIGINT'); + } + this.results.video = this.videoPath; + } + } +} \ No newline at end of file diff --git a/src/runners/oxygen/index.js b/src/runners/oxygen/index.js index 11b589fc..418f1185 100644 --- a/src/runners/oxygen/index.js +++ b/src/runners/oxygen/index.js @@ -548,6 +548,7 @@ export default class OxygenRunner extends EventEmitter { caseResult.steps = resultStore && resultStore.steps ? resultStore.steps : []; caseResult.logs = resultStore && resultStore.logs ? resultStore.logs : []; caseResult.har = resultStore && resultStore.har ? resultStore.har : null; + caseResult.video = resultStore && resultStore.video ? resultStore.video : null; // determine test case iteration status - mark it as failed if any step has failed var failedSteps = _.find(caseResult.steps, {status: Status.FAILED});