diff --git a/.nycrc.js b/.nycrc.js index 38f71f632..a69e0c262 100644 --- a/.nycrc.js +++ b/.nycrc.js @@ -32,7 +32,7 @@ function configOverrides (testType) { statements: 70, branches: 55, functions: 75, - lines: 75 + lines: 70 }; default: return {} diff --git a/CHANGELOG.yaml b/CHANGELOG.yaml index 10846b4b3..9377a47d5 100644 --- a/CHANGELOG.yaml +++ b/CHANGELOG.yaml @@ -3,6 +3,10 @@ master: - >- GH-2806 Exposed the runner on newman and the runtime run object in start event + - >- + Added ability to see request and responses in CLI reporter `--verbose` + mode. Also added ability to see additional meta such as header count, + cookie count and body mime-types in verbose mode. chores: - Updated dependencies diff --git a/lib/reporters/cli/cli-utils-symbols.js b/lib/reporters/cli/cli-utils-symbols.js index 36d3b5bdf..191dc8ad3 100644 --- a/lib/reporters/cli/cli-utils-symbols.js +++ b/lib/reporters/cli/cli-utils-symbols.js @@ -19,7 +19,10 @@ subsets = { root: '→', sub: '↳', ok: '✓', - error: '✖' + error: '✖', + star: '★', + up: '↑', + down: '↓' }, encoded: { console: { @@ -32,7 +35,10 @@ subsets = { root: '\u2192', sub: '\u2514', ok: '\u221A', - error: '\u00D7' + error: '\u00D7', + star: '\u2605', + up: '\u2191', + down: '\u2193' }, plainText: { console: { @@ -45,7 +51,10 @@ subsets = { root: 'Root', sub: 'Sub-folder', ok: 'Pass', - error: 'Fail' + error: 'Fail', + star: '*', + up: '^', + down: 'v' } }; diff --git a/lib/reporters/cli/index.js b/lib/reporters/cli/index.js index af7eeffd4..3728a2874 100644 --- a/lib/reporters/cli/index.js +++ b/lib/reporters/cli/index.js @@ -15,6 +15,18 @@ var _ = require('lodash'), E = '', CACHED_TIMING_PHASE = '(cache)', + TIMING_TABLE_HEADERS = { + prepare: 'prepare', + wait: 'wait', + dns: 'dns-lookup', + tcp: 'tcp-handshake', + secureHandshake: 'ssl-handshake', + firstByte: 'transfer-start', + download: 'download', + process: 'process', + total: 'total' + }, + BODY_CLIP_SIZE = 2048, PostmanCLIReporter, timestamp, @@ -175,53 +187,133 @@ PostmanCLIReporter = function (emitter, reporterOptions, options) { return; } - var timingTable, - timings, - timingPhases, - size = o.response && o.response.size(), - timingHeaders = { - prepare: 'prepare', - wait: 'wait', - dns: 'dns-lookup', - tcp: 'tcp-handshake', - secureHandshake: 'ssl-handshake', - firstByte: 'transfer-start', - download: 'download', - process: 'process', - total: 'total' - }; - - size = size && (size.header || 0) + (size.body || 0) || 0; - - print.lf(colors.gray('[%d %s, %s, %s]'), o.response.code, o.response.reason(), - util.filesize(size), util.prettyms(o.response.responseTime)); - - // if there are redirects, get timings for the last request sent - timings = _.last(_.get(o, 'history.execution.data')); - timings = timings && timings.timings; + if (!(o.request && o.response)) { + print.lf(colors.red('[errored]')); + print.lf(colors.red(' %s'), 'Internal error! Could not read response data.'); - // print timing info of the request - if (options.verbose && timings) { - timingPhases = util.beautifyTime(sdk.Response.timingPhases(timings)); + return; + } + + // quickly print out basic non verbose response meta and exit + if (!options.verbose) { + print.lf(colors.gray('[%d %s, %s, %s]'), o.response.code, o.response.reason(), + util.filesize(o.response.size().total), util.prettyms(o.response.responseTime)); - timingTable = new Table({ - chars: _.defaults({ mid: '', middle: '' }, cliUtils.cliTableTemplate_Blank), - colAligns: _.fill(Array(_.size(timingPhases)), 'left'), - style: { 'padding-left': 2 } - }); + return; + } - timingPhases = _.transform(timingHeaders, (result, header, key) => { + // this point onwards the output is verbose. a tonne of variables are created here for + // keeping the output clean and readable + + let req = o.request, + res = o.response, + + // set values here with abundance of caution to avoid erroring out + reqSize = util.filesize(req.size().total), + resSize = util.filesize(res.size().total), + code = res.code, + reason = res.reason(), + mime = res.contentInfo() || {}, + timings = _.last(_.get(o, 'history.execution.data')), + + reqHeadersLen = _.get(req, 'headers.members.length'), + resHeadersLen = _.get(res, 'headers.members.length'), + + resTime = util.prettyms(res.responseTime || 0), + + reqText = (options.verbose && req.body) ? req.body.toString() : E, + reqTextLen = req.size().body || Buffer.byteLength(reqText), + + resText = options.verbose ? res.text() : E, + resTextLen = res.size().body || Buffer.byteLength(resText), + + reqBodyMode = _.get(req, 'body.mode', ''), + resSummary = [ + `${mime.contentType}`, + `${mime.mimeType}`, + `${mime.mimeFormat}`, + `${mime.charset}` + ].join(` ${colors.gray(symbols.star)} `); + + + print.lf(SPC); // also flushes out the circling progress icon + + // for clean readability of code. this section compiles the cli string for one line of + // req-res combined summary. this looks somewhat like below: + // >> 200 OK ★ 979ms time ★ 270B↑ 793B↓ size ★ 7↑ 7↓ headers ★ 0 cookies + print.lf(SPC + SPC + [ + `${code} ${reason}`, + `${resTime} ${colors.gray('time')}`, + `${reqSize}${colors.gray(symbols.up)} ${resSize}${colors.gray(symbols.down)} ${colors.gray('size')}`, + `${reqHeadersLen}${colors.gray(symbols.up)} ` + + `${resHeadersLen}${colors.gray(symbols.down)} ${colors.gray('headers')}`, + `${_.get(res, 'cookies.members.length')} ${colors.gray('cookies')}` + ].join(` ${colors.gray(symbols.star)} `)); + + // print request body + if (reqTextLen) { + // truncate very large request (is 2048 large enough?) + if (reqTextLen > BODY_CLIP_SIZE) { + reqText = reqText.substr(0, BODY_CLIP_SIZE) + + colors.brightWhite(`\n(showing ${util.filesize(BODY_CLIP_SIZE)}/${util.filesize(reqTextLen)})`); + } + + reqText = wrap(reqText, ` ${colors.white(symbols.console.middle)} `); + // eslint-disable-next-line max-len + print.buffer(` ${colors.white(symbols.console.top)} ${colors.white(symbols.up)} ${reqBodyMode} ${colors.gray(symbols.star)} ${util.filesize(reqTextLen)}\n`, + colors.white(` ${symbols.console.bottom}`)) + // tweak the message to ensure that its surrounding is not brightly coloured. + // also ensure to remove any blank lines generated due to util.inspect + .nobuffer(colors.gray(reqText.replace(/\n\s*\n/g, LF) + LF)); + + print.lf(SPC); // visual tweak: flushes out the buffer of wrapping body above + } + + // print response body + if (resTextLen) { + // truncate very large response (is 2048 large enough?) + if (resTextLen > BODY_CLIP_SIZE) { + resText = resText.substr(0, BODY_CLIP_SIZE) + + colors.brightWhite(`\n(showing ${util.filesize(BODY_CLIP_SIZE)}/${util.filesize(resTextLen)})`); + } + + resText = wrap(resText, ` ${colors.white(symbols.console.middle)} `); + // eslint-disable-next-line max-len + print.buffer(` ${colors.white(symbols.console.top)} ${colors.white(symbols.down)} ${resSummary} ${colors.gray(symbols.star)} ${util.filesize(resTextLen)}\n`, + colors.white(` ${symbols.console.bottom}`)) + // tweak the message to ensure that its surrounding is not brightly coloured. + // also ensure to remove any blank lines generated due to util.inspect + .nobuffer(colors.gray(resText.replace(/\n\s*\n/g, LF) + LF)); + } + // print the line of response body meta one liner if there is no response body + // if there is one, we would already print it across the body braces above. + else { + // we need to do some newline related shenanigans here so that the output looks clean + // in the absence of the request body block + print.lf(` ${symbols.down} ${resSummary}`); + } + + // print timing info of the request + timings = timings && timings.timings; // if there are redirects, get timings for the last request sent + if (timings) { + // adds nice units to all time data in the object + let timingPhases = util.beautifyTime(sdk.Response.timingPhases(timings)), + timingTable = new Table({ + chars: _.defaults({ mid: '', middle: '' }, cliUtils.cliTableTemplate_Blank), + colAligns: _.fill(Array(_.size(timingPhases)), 'left'), + style: { 'padding-left': 2 } + }); + + timingPhases = _.transform(TIMING_TABLE_HEADERS, (result, header, key) => { if (_.has(timingPhases, key)) { result.headers.push(colors.white(header)); result.values.push(colors.log(timingPhases[key] || CACHED_TIMING_PHASE)); } }, { headers: [], values: [] }); - // add name of phases in the table - timingTable.push(timingPhases.headers); - // add time of phases in the table - timingTable.push(timingPhases.values); + timingTable.push(timingPhases.headers); // add name of phases in the table + timingTable.push(timingPhases.values); // add time of phases in the table print(LF + timingTable + LF + LF); } diff --git a/test/unit/cli-reporter-symbols.test.js b/test/unit/cli-reporter-symbols.test.js index 809136b76..31ff6e920 100644 --- a/test/unit/cli-reporter-symbols.test.js +++ b/test/unit/cli-reporter-symbols.test.js @@ -1,52 +1,49 @@ +const _ = require('lodash'), + + isDoubleByte = function (str) { + for (var i = 0, n = str.length; i < n; i++) { + if (str.charCodeAt(i) > 255) { return true; } + } + + return false; + }; + +/* eslint-disable */ +/** + * @attribution https://github.com/lodash/lodash/issues/2240#issuecomment-418820848 + */ +const flattenKeys = (obj, path = []) => + !_.isObject(obj) + ? { [path.join('.')]: obj } + : _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {}); +/* eslint-enable */ + describe('unicode handling of cli symbol utility module', function () { - var cliUtilsSymbols = require('../../lib/reporters/cli/cli-utils-symbols.js'); - - it('should revert to text alternatives when disableUnicode parameter is set to true', function () { - var symbols = cliUtilsSymbols(true); - - expect(symbols).to.eql({ - console: { - top: '-', - middle: '|', - bottom: '-' - }, - dot: '.', - folder: 'Folder', - root: 'Root', - sub: 'Sub-folder', - ok: 'Pass', - error: 'Fail' - }); + let cliUtilsSymbols = require('../../lib/reporters/cli/cli-utils-symbols.js'); + + it('should have three symbol classes', function () { + expect(cliUtilsSymbols(true)).to.be.an('object'); + expect(cliUtilsSymbols(false)).to.be.an('object'); }); - it('should provide the platform-specific default symbol map when no options are passed', function () { - var symbols = cliUtilsSymbols(), - isWin = (/^win/).test(process.platform); - - expect(symbols).to.eql(isWin ? { - console: { - top: '\u250C', - middle: '\u2502', - bottom: '\u2514' - }, - dot: '.', - folder: '□', - root: '→', - sub: '└', - ok: '√', - error: '×' - } : { - console: { - top: '┌', - middle: '│', - bottom: '└' - }, - dot: '.', - folder: '❏', - root: '→', - sub: '↳', - ok: '✓', - error: '✖' - }); + it('should have appropriate fallback for unicode', function () { + let symbol = cliUtilsSymbols(true), + fallback = cliUtilsSymbols(false), + + flattenedSymbols = flattenKeys(symbol), + flattenedPlainSymbols = flattenKeys(fallback); + + expect(flattenedSymbols).to.contain.keys(flattenedPlainSymbols); + }); + + it('should not have unicode when not requested', function () { + let fallbackSymbols = cliUtilsSymbols(true), + flattenedPlainSymbols = flattenKeys(fallbackSymbols); + + // first compute all double byte checking + for (const [key, value] of Object.entries(flattenedPlainSymbols)) { + // @todo make the assertion better so that failure here says what exactly went wrong + expect(isDoubleByte(value)).to.eql(false, key); + } }); }); diff --git a/test/unit/options.test.js b/test/unit/options.test.js index aa875fd97..9ba1dd916 100644 --- a/test/unit/options.test.js +++ b/test/unit/options.test.js @@ -1,4 +1,5 @@ var _ = require('lodash'), + { VariableScope } = require('postman-collection'), options = require('../../lib/run/options'); describe('options', function () { @@ -34,6 +35,18 @@ describe('options', function () { }); }); + it('should apply directly specified env variables to environment list', function (done) { + options({ + envVar: [{ key: 'test', value: 'data' }] + }, function (err, result) { + expect(err).to.be.null; + expect(result).to.have.property('environment'); + expect(result.environment).to.be.an.instanceof(VariableScope); + expect(result.environment.get('test')).to.equal('data'); + done(); + }); + }); + it('should be handled correctly for globals', function (done) { var globals = require('../../test/fixtures/run/spaces/simple-variables.json'); @@ -47,6 +60,18 @@ describe('options', function () { }); }); + it('should apply directly specified global variables to globals list', function (done) { + options({ + globalVar: [{ key: 'test', value: 'data' }] + }, function (err, result) { + expect(err).to.be.null; + expect(result).to.have.property('globals'); + expect(result.globals).to.be.an.instanceof(VariableScope); + expect(result.globals.get('test')).to.equal('data'); + done(); + }); + }); + it('should be handled correctly for iterationData', function (done) { var data = require('../../test/fixtures/run/spaces/data.json'); diff --git a/test/unit/util.test.js b/test/unit/util.test.js index 48a73e369..c07c09540 100644 --- a/test/unit/util.test.js +++ b/test/unit/util.test.js @@ -60,6 +60,19 @@ describe('utility helpers', function () { }); }); + describe('type checkers', function () { + it('should validate integers', function () { + expect(util.isInt('123')).to.be.true; + expect(util.isInt('123.5')).to.be.false; + }); + + // @todo some issue with the function or my understanding of it's usage + it.skip('should validate floating point', function () { + expect(util.isFloat('123')).to.be.false; + expect(util.isFloat('123.5')).to.be.true; + }); + }); + describe('beautifyTime', function () { var timings = { wait: 1.4010989999997037,